From ff4c7b7e0917beba8b3f88dd22669c055d49b0d6 Mon Sep 17 00:00:00 2001 From: zhiyun Date: Mon, 2 Feb 2026 18:21:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E4=BB=A3=E7=A0=81=20-=202026-02-02?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 111 + app.js | 537 +++++ app.json | 117 + app.wxss | 200 ++ components/icon/icon.js | 61 + components/icon/icon.json | 3 + components/icon/icon.wxml | 1 + components/icon/icon.wxss | 6 + config/index.js | 59 + git-push.bat | 34 + git-push.sh | 33 + images/README.md | 74 + images/btn-consult.png | Bin 0 -> 1251 bytes images/btn-text-consult.png | Bin 0 -> 13220 bytes images/chat-action-camera.png | Bin 0 -> 13387 bytes images/chat-action-gift.png | Bin 0 -> 13004 bytes images/chat-action-photo.png | Bin 0 -> 13095 bytes images/chat-input-emoji.png | Bin 0 -> 4117 bytes images/chat-input-plus.png | Bin 0 -> 2294 bytes images/chat-input-voice.png | Bin 0 -> 3431 bytes images/default-avatar.svg | 5 + images/figma-gift-shop-chevron-right.svg | 3 + images/figma-gift-shop-decor-icon.svg | 36 + images/figma-gift-shop-love-icon.svg | 11 + images/figma-gift-shop-price-icon.svg | 10 + images/figma-gift-shop-section-icon.svg | 5 + images/fuw-aixin.png | Bin 0 -> 13206 bytes images/fuw-dingzhi.png | Bin 0 -> 12922 bytes images/fuw-kangyang.png | Bin 0 -> 12182 bytes images/fuw-pinpai.png | Bin 0 -> 12750 bytes images/fuw-shangcheng.png | Bin 0 -> 12535 bytes images/fuw-shangjia.png | Bin 0 -> 12547 bytes images/gift-arrow.png | Bin 0 -> 955 bytes images/gift-balloon.png | Bin 0 -> 964 bytes images/gift-cake.png | Bin 0 -> 786 bytes images/gift-cheer.png | Bin 0 -> 1663 bytes images/gift-chicken.png | Bin 0 -> 1196 bytes images/gift-cup.png | Bin 0 -> 1033 bytes images/gift-diamond.png | Bin 0 -> 1067 bytes images/gift-heart.png | Bin 0 -> 715 bytes images/gift-hug.png | Bin 0 -> 1207 bytes images/gift-milk.png | Bin 0 -> 779 bytes images/gift-pot.png | Bin 0 -> 1025 bytes images/gift-rice.png | Bin 0 -> 851 bytes images/gift-rose.png | Bin 0 -> 973 bytes images/gift-scarf.png | Bin 0 -> 784 bytes images/icon-arrow-right-pink.png | 1 + images/icon-arrow-right.png | 5 + images/icon-back-arrow.png | Bin 0 -> 298 bytes images/icon-back-arrow2.png | 5 + images/icon-back-line.png | Bin 0 -> 175 bytes images/icon-back.png | Bin 0 -> 262 bytes images/icon-backpack.png | Bin 0 -> 721 bytes images/icon-bell.png | Bin 0 -> 913 bytes images/icon-calendar.png | Bin 0 -> 942 bytes images/icon-camera.png | Bin 0 -> 588 bytes images/icon-check.png | Bin 0 -> 282 bytes images/icon-checkin.png | Bin 0 -> 6202 bytes images/icon-chevron-down.png | Bin 0 -> 282 bytes images/icon-chevron-left.png | Bin 0 -> 246 bytes images/icon-chevron-right.png | Bin 0 -> 244 bytes images/icon-city.png | Bin 0 -> 8180 bytes images/icon-cleaning.png | 6 + images/icon-clock.png | Bin 0 -> 942 bytes images/icon-close.png | Bin 0 -> 363 bytes images/icon-comment.png | Bin 0 -> 1226 bytes images/icon-companion.png | Bin 0 -> 893 bytes images/icon-cooperation.png | 1 + images/icon-coupon.png | Bin 0 -> 457 bytes images/icon-edit.png | Bin 0 -> 694 bytes images/icon-eldercare.png | 1 + images/icon-elderly.png | 13 + images/icon-emoji-face.png | Bin 0 -> 1979 bytes images/icon-emoji-gray.svg | 6 + images/icon-emoji.png | Bin 0 -> 1246 bytes images/icon-empty.png | Bin 0 -> 889 bytes images/icon-errand.png | 7 + images/icon-eye.png | Bin 0 -> 980 bytes images/icon-feedback.png | Bin 0 -> 442 bytes images/icon-gift.png | Bin 0 -> 767 bytes images/icon-grass.png | Bin 0 -> 928 bytes images/icon-headphones.png | Bin 0 -> 883 bytes images/icon-heart-filled.png | Bin 0 -> 681 bytes images/icon-heart-listen.png | 1 + images/icon-heart-new.png | Bin 0 -> 23970 bytes images/icon-heart.png | Bin 0 -> 681 bytes images/icon-heart_.png | Bin 0 -> 384 bytes images/icon-help.png | Bin 0 -> 1348 bytes images/icon-hospital.png | 9 + images/icon-housekeeping.png | 1 + images/icon-info.png | Bin 0 -> 1123 bytes images/icon-interest.png | Bin 0 -> 7032 bytes images/icon-keyboard.png | Bin 0 -> 456 bytes images/icon-legal.png | 9 + images/icon-listen.png | 8 + images/icon-location-pin.png | Bin 0 -> 1307 bytes images/icon-location.png | Bin 0 -> 745 bytes images/icon-lock.png | Bin 0 -> 680 bytes images/icon-logout.png | Bin 0 -> 457 bytes images/icon-love.png | Bin 0 -> 6063 bytes images/icon-medical.png | 1 + images/icon-mic.png | Bin 0 -> 674 bytes images/icon-more-dot.png | Bin 0 -> 798 bytes images/icon-more.png | Bin 0 -> 239 bytes images/icon-notification.png | 8 + images/icon-order-complete.png | Bin 0 -> 1236 bytes images/icon-order-paid.png | Bin 0 -> 517 bytes images/icon-order.png | Bin 0 -> 445 bytes images/icon-outdoor.png | Bin 0 -> 7796 bytes images/icon-phone-white.png | Bin 0 -> 938 bytes images/icon-phone.png | Bin 0 -> 933 bytes images/icon-plus.png | Bin 0 -> 197 bytes images/icon-promote.png | Bin 0 -> 718 bytes images/icon-refresh.png | Bin 0 -> 1026 bytes images/icon-search.png | Bin 0 -> 972 bytes images/icon-send.png | Bin 0 -> 550 bytes images/icon-settings.png | Bin 0 -> 1452 bytes images/icon-share.png | Bin 0 -> 781 bytes images/icon-star.png | Bin 0 -> 841 bytes images/icon-test.png | Bin 0 -> 622 bytes images/icon-travel.png | Bin 0 -> 7774 bytes images/icon-trending-up.png | Bin 0 -> 511 bytes images/icon-users.png | Bin 0 -> 904 bytes images/icon-verified.png | Bin 0 -> 1067 bytes images/icon-vip.png | Bin 0 -> 740 bytes images/icon-voice.png | Bin 0 -> 408 bytes images/icon-wallet.png | Bin 0 -> 353 bytes images/service-arrow-right.png | Bin 0 -> 483 bytes images/service-clock.png | Bin 0 -> 1056 bytes images/service-notification.png | Bin 0 -> 2154 bytes images/service-search.png | Bin 0 -> 1212 bytes images/service-star.png | Bin 0 -> 618 bytes images/service-type-custom.png | Bin 0 -> 24523 bytes images/service-type-eldercare.png | Bin 0 -> 23428 bytes images/service-type-housekeeping.png | Bin 0 -> 23558 bytes images/service-type-leisure.png | Bin 0 -> 23824 bytes images/service-type-listen.png | Bin 0 -> 24937 bytes images/service-type-medical.png | Bin 0 -> 24634 bytes images/tab-chat-active.png | Bin 0 -> 934 bytes images/tab-chat.png | Bin 0 -> 929 bytes images/tab-companion-active.png | Bin 0 -> 2908 bytes images/tab-companion.png | Bin 0 -> 2784 bytes images/tab-compass-active.png | Bin 0 -> 1645 bytes images/tab-compass.png | Bin 0 -> 1717 bytes images/tab-heart-active.png | Bin 0 -> 1015 bytes images/tab-heart.png | Bin 0 -> 1517 bytes images/tab-listen-active.png | Bin 0 -> 750 bytes images/tab-listen-new.png | Bin 0 -> 1382 bytes images/tab-listen.png | Bin 0 -> 2199 bytes images/tab-message-active-nodot.png | Bin 0 -> 1514 bytes images/tab-message-active.png | Bin 0 -> 3353 bytes images/tab-message-nodot.png | Bin 0 -> 1911 bytes images/tab-message.png | Bin 0 -> 2482 bytes images/tab-profile-active.png | Bin 0 -> 2136 bytes images/tab-profile.png | Bin 0 -> 2081 bytes images/tab-service-active.png | Bin 0 -> 2894 bytes images/tab-service.png | Bin 0 -> 2615 bytes images/tab-user-active.png | Bin 0 -> 907 bytes images/tab-user.png | Bin 0 -> 1054 bytes images/wenyu-type-02.png | Bin 0 -> 8699 bytes pages/academy/detail/detail.js | 68 + pages/academy/detail/detail.json | 6 + pages/academy/detail/detail.wxml | 33 + pages/academy/detail/detail.wxss | 127 + pages/academy/list/list.js | 156 ++ pages/academy/list/list.json | 6 + pages/academy/list/list.wxml | 56 + pages/academy/list/list.wxss | 180 ++ pages/activity-detail/activity-detail.js | 767 ++++++ pages/activity-detail/activity-detail.json | 9 + pages/activity-detail/activity-detail.wxml | 229 ++ pages/activity-detail/activity-detail.wxss | 815 +++++++ pages/agreement/agreement.js | 76 + pages/agreement/agreement.json | 4 + pages/agreement/agreement.wxml | 20 + pages/agreement/agreement.wxss | 64 + pages/backpack/backpack.js | 54 + pages/backpack/backpack.json | 5 + pages/backpack/backpack.wxml | 31 + pages/backpack/backpack.wxss | 121 + pages/brand/brand.js | 51 + pages/brand/brand.json | 3 + pages/brand/brand.wxml | 39 + pages/brand/brand.wxss | 106 + pages/character-detail/character-detail.js | 657 ++++++ pages/character-detail/character-detail.json | 4 + pages/character-detail/character-detail.wxml | 143 ++ pages/character-detail/character-detail.wxss | 690 ++++++ pages/chat-detail/chat-detail.js | 2095 +++++++++++++++++ pages/chat-detail/chat-detail.json | 4 + pages/chat-detail/chat-detail.wxml | 406 ++++ pages/chat-detail/chat-detail.wxss | 1659 +++++++++++++ pages/chat/chat.js | 714 ++++++ pages/chat/chat.json | 4 + pages/chat/chat.wxml | 108 + pages/chat/chat.wxss | 292 +++ pages/city-activities/city-activities.js | 358 +++ pages/city-activities/city-activities.json | 7 + pages/city-activities/city-activities.wxml | 128 + pages/city-activities/city-activities.wxss | 479 ++++ pages/city-selector/city-selector.js | 151 ++ pages/city-selector/city-selector.json | 4 + pages/city-selector/city-selector.wxml | 57 + pages/city-selector/city-selector.wxss | 137 ++ pages/commission/commission.js | 666 ++++++ pages/commission/commission.json | 6 + pages/commission/commission.wxml | 88 + pages/commission/commission.wxss | 266 +++ pages/companion-apply/ai.code-workspace | 11 + pages/companion-apply/companion-apply.js | 220 ++ pages/companion-apply/companion-apply.json | 5 + pages/companion-apply/companion-apply.wxml | 115 + pages/companion-apply/companion-apply.wxss | 453 ++++ pages/companion-chat/companion-chat.js | 751 ++++++ pages/companion-chat/companion-chat.json | 4 + pages/companion-chat/companion-chat.wxml | 236 ++ pages/companion-chat/companion-chat.wxss | 817 +++++++ pages/companion-orders/companion-orders.js | 307 +++ pages/companion-orders/companion-orders.json | 5 + pages/companion-orders/companion-orders.wxml | 164 ++ pages/companion-orders/companion-orders.wxss | 488 ++++ .../cooperation-applications.js | 71 + .../cooperation-applications.json | 3 + .../cooperation-applications.wxml | 2 + .../cooperation-applications.wxss | 1 + pages/counselor-detail/counselor-detail.js | 1171 +++++++++ pages/counselor-detail/counselor-detail.json | 5 + pages/counselor-detail/counselor-detail.wxml | 525 +++++ pages/counselor-detail/counselor-detail.wxss | 1738 ++++++++++++++ pages/custom/custom.js | 46 + pages/custom/custom.json | 3 + pages/custom/custom.wxml | 38 + pages/custom/custom.wxss | 165 ++ .../customer-management.js | 233 ++ .../customer-management.json | 4 + .../customer-management.wxml | 132 ++ .../customer-management.wxss | 508 ++++ pages/edit-profile/edit-profile.js | 323 +++ pages/edit-profile/edit-profile.json | 5 + pages/edit-profile/edit-profile.wxml | 100 + pages/edit-profile/edit-profile.wxss | 172 ++ pages/eldercare-apply/eldercare-apply.js | 162 ++ pages/eldercare-apply/eldercare-apply.json | 5 + pages/eldercare-apply/eldercare-apply.wxml | 191 ++ pages/eldercare-apply/eldercare-apply.wxss | 94 + pages/eldercare/eldercare.js | 46 + pages/eldercare/eldercare.json | 3 + pages/eldercare/eldercare.wxml | 38 + pages/eldercare/eldercare.wxss | 210 ++ .../entertainment-apply.js | 162 ++ .../entertainment-apply.json | 5 + .../entertainment-apply.wxml | 103 + .../entertainment-apply.wxss | 169 ++ pages/entertainment/entertainment.js | 894 +++++++ pages/entertainment/entertainment.json | 8 + pages/entertainment/entertainment.wxml | 230 ++ pages/entertainment/entertainment.wxss | 729 ++++++ pages/gift-detail/gift-detail.js | 185 ++ pages/gift-detail/gift-detail.json | 5 + pages/gift-detail/gift-detail.wxml | 55 + pages/gift-detail/gift-detail.wxss | 161 ++ pages/gift-exchanges/gift-exchanges.js | 113 + pages/gift-exchanges/gift-exchanges.json | 7 + pages/gift-exchanges/gift-exchanges.wxml | 61 + pages/gift-exchanges/gift-exchanges.wxss | 185 ++ pages/gift-shop/gift-shop.js | 141 ++ pages/gift-shop/gift-shop.json | 5 + pages/gift-shop/gift-shop.wxml | 93 + pages/gift-shop/gift-shop.wxss | 387 +++ pages/happy-school/happy-school.js | 394 ++++ pages/happy-school/happy-school.json | 9 + pages/happy-school/happy-school.wxml | 172 ++ pages/happy-school/happy-school.wxss | 486 ++++ .../housekeeping-apply/housekeeping-apply.js | 260 ++ .../housekeeping-apply.json | 5 + .../housekeeping-apply.wxml | 267 +++ .../housekeeping-apply.wxss | 299 +++ pages/index/index.js | 1586 +++++++++++++ pages/index/index.json | 4 + pages/index/index.wxml | 276 +++ pages/index/index.wxss | 1061 +++++++++ pages/interest-partner/interest-partner.js | 281 +++ pages/interest-partner/interest-partner.json | 6 + pages/interest-partner/interest-partner.wxml | 106 + pages/interest-partner/interest-partner.wxss | 489 ++++ pages/invite/invite.js | 662 ++++++ pages/invite/invite.json | 7 + pages/invite/invite.wxml | 107 + pages/invite/invite.wxss | 488 ++++ pages/login/login.js | 236 ++ pages/login/login.json | 5 + pages/login/login.wxml | 56 + pages/login/login.wxss | 194 ++ pages/love-transactions/love-transactions.js | 143 ++ .../love-transactions/love-transactions.json | 5 + .../love-transactions/love-transactions.wxml | 69 + .../love-transactions/love-transactions.wxss | 246 ++ pages/medical-apply/medical-apply.js | 315 +++ pages/medical-apply/medical-apply.json | 5 + pages/medical-apply/medical-apply.wxml | 267 +++ pages/medical-apply/medical-apply.wxss | 374 +++ pages/my-activities/my-activities.js | 118 + pages/my-activities/my-activities.json | 8 + pages/my-activities/my-activities.wxml | 75 + pages/my-activities/my-activities.wxss | 224 ++ pages/notices/detail/detail.js | 64 + pages/notices/detail/detail.json | 3 + pages/notices/detail/detail.wxml | 32 + pages/notices/detail/detail.wxss | 118 + pages/notices/notices.js | 77 + pages/notices/notices.json | 3 + pages/notices/notices.wxml | 43 + pages/notices/notices.wxss | 140 ++ pages/order-detail/order-detail.js | 331 +++ pages/order-detail/order-detail.json | 4 + pages/order-detail/order-detail.wxml | 152 ++ pages/order-detail/order-detail.wxss | 421 ++++ pages/orders/orders.js | 118 + pages/orders/orders.json | 7 + pages/orders/orders.wxml | 40 + pages/orders/orders.wxss | 136 ++ .../outdoor-activities/outdoor-activities.js | 374 +++ .../outdoor-activities.json | 9 + .../outdoor-activities.wxml | 133 ++ .../outdoor-activities.wxss | 603 +++++ pages/performance/performance.js | 214 ++ pages/performance/performance.json | 6 + pages/performance/performance.wxml | 85 + pages/performance/performance.wxss | 208 ++ pages/profile/profile.js | 640 +++++ pages/profile/profile.json | 5 + pages/profile/profile.wxml | 326 +++ pages/profile/profile.wxss | 783 ++++++ pages/promote-poster/promote-poster.js | 247 ++ pages/promote-poster/promote-poster.json | 3 + pages/promote-poster/promote-poster.wxml | 39 + pages/promote-poster/promote-poster.wxss | 117 + pages/promote/promote.js | 386 +++ pages/promote/promote.json | 7 + pages/promote/promote.wxml | 123 + pages/promote/promote.wxss | 338 +++ pages/recharge/recharge.js | 151 ++ pages/recharge/recharge.json | 5 + pages/recharge/recharge.wxml | 101 + pages/recharge/recharge.wxss | 525 +++++ pages/referrals/orders/orders.js | 107 + pages/referrals/orders/orders.json | 4 + pages/referrals/orders/orders.wxml | 61 + pages/referrals/orders/orders.wxss | 154 ++ pages/referrals/referrals.js | 265 +++ pages/referrals/referrals.json | 6 + pages/referrals/referrals.wxml | 111 + pages/referrals/referrals.wxss | 306 +++ .../service-provider-detail.js | 318 +++ .../service-provider-detail.json | 4 + .../service-provider-detail.wxml | 308 +++ .../service-provider-detail.wxss | 920 ++++++++ pages/service/service.js | 293 +++ pages/service/service.json | 5 + pages/service/service.wxml | 98 + pages/service/service.wxss | 570 +++++ pages/settings/settings.js | 72 + pages/settings/settings.json | 5 + pages/settings/settings.wxml | 31 + pages/settings/settings.wxss | 141 ++ pages/singles-party/singles-party.js | 326 +++ pages/singles-party/singles-party.json | 7 + pages/singles-party/singles-party.wxml | 135 ++ pages/singles-party/singles-party.wxss | 557 +++++ pages/square/square.js | 287 +++ pages/square/square.json | 4 + pages/square/square.wxml | 191 ++ pages/square/square.wxss | 610 +++++ pages/support/support.js | 214 ++ pages/support/support.json | 7 + pages/support/support.wxml | 125 + pages/support/support.wxss | 310 +++ pages/team/team.js | 147 ++ pages/team/team.json | 5 + pages/team/team.wxml | 61 + pages/team/team.wxss | 175 ++ .../theme-travel-apply/theme-travel-apply.js | 162 ++ .../theme-travel-apply.json | 4 + .../theme-travel-apply.wxml | 105 + .../theme-travel-apply.wxss | 201 ++ pages/theme-travel/theme-travel.js | 455 ++++ pages/theme-travel/theme-travel.json | 9 + pages/theme-travel/theme-travel.wxml | 134 ++ pages/theme-travel/theme-travel.wxss | 607 +++++ pages/webview/webview.js | 34 + pages/webview/webview.json | 4 + pages/webview/webview.wxml | 15 + pages/webview/webview.wxss | 58 + pages/withdraw-records/withdraw-records.js | 186 ++ pages/withdraw-records/withdraw-records.json | 6 + pages/withdraw-records/withdraw-records.wxml | 116 + pages/withdraw-records/withdraw-records.wxss | 303 +++ pages/withdraw/withdraw.js | 132 ++ pages/withdraw/withdraw.json | 5 + pages/withdraw/withdraw.wxml | 83 + pages/withdraw/withdraw.wxss | 302 +++ pages/workbench/workbench.js | 320 +++ pages/workbench/workbench.json | 5 + pages/workbench/workbench.wxml | 175 ++ pages/workbench/workbench.wxss | 509 ++++ project.config.json | 58 + project.private.config.json | 24 + sitemap.json | 7 + subpackages/cooperation/images/icon_back.png | Bin 0 -> 490 bytes .../cooperation/images/icon_chevron_gray.png | Bin 0 -> 394 bytes .../cooperation/images/icon_chevron_white.png | Bin 0 -> 363 bytes .../cooperation/images/icon_eldercare.png | Bin 0 -> 3679 bytes .../cooperation/images/icon_entertainment.png | Bin 0 -> 2490 bytes .../cooperation/images/icon_housekeeping.png | Bin 0 -> 2855 bytes .../cooperation/images/icon_medical.png | Bin 0 -> 3243 bytes .../cooperation/images/icon_orders.png | Bin 0 -> 2203 bytes .../pages/cooperation/cooperation.js | 182 ++ .../pages/cooperation/cooperation.json | 5 + .../pages/cooperation/cooperation.wxml | 60 + .../pages/cooperation/cooperation.wxss | 195 ++ team_page_redesign.md | 48 + utils/api.js | 1856 +++++++++++++++ utils/assets.js | 193 ++ utils/auth.js | 464 ++++ utils/errorHandler.js | 237 ++ utils/imageUrl.js | 121 + utils/payment.js | 321 +++ utils/proactiveMessage.js | 270 +++ utils/util.js | 317 +++ utils_new/auth.js | 51 + utils_new/payment.js | 93 + utils_new/request.js | 57 + 前端样式修复经验.md | 113 + 小程序前端开发与后端API对接经验汇总.md | 219 ++ 434 files changed, 58988 insertions(+) create mode 100644 README.md create mode 100644 app.js create mode 100644 app.json create mode 100644 app.wxss create mode 100644 components/icon/icon.js create mode 100644 components/icon/icon.json create mode 100644 components/icon/icon.wxml create mode 100644 components/icon/icon.wxss create mode 100644 config/index.js create mode 100644 git-push.bat create mode 100644 git-push.sh create mode 100644 images/README.md create mode 100644 images/btn-consult.png create mode 100644 images/btn-text-consult.png create mode 100644 images/chat-action-camera.png create mode 100644 images/chat-action-gift.png create mode 100644 images/chat-action-photo.png create mode 100644 images/chat-input-emoji.png create mode 100644 images/chat-input-plus.png create mode 100644 images/chat-input-voice.png create mode 100644 images/default-avatar.svg create mode 100644 images/figma-gift-shop-chevron-right.svg create mode 100644 images/figma-gift-shop-decor-icon.svg create mode 100644 images/figma-gift-shop-love-icon.svg create mode 100644 images/figma-gift-shop-price-icon.svg create mode 100644 images/figma-gift-shop-section-icon.svg create mode 100644 images/fuw-aixin.png create mode 100644 images/fuw-dingzhi.png create mode 100644 images/fuw-kangyang.png create mode 100644 images/fuw-pinpai.png create mode 100644 images/fuw-shangcheng.png create mode 100644 images/fuw-shangjia.png create mode 100644 images/gift-arrow.png create mode 100644 images/gift-balloon.png create mode 100644 images/gift-cake.png create mode 100644 images/gift-cheer.png create mode 100644 images/gift-chicken.png create mode 100644 images/gift-cup.png create mode 100644 images/gift-diamond.png create mode 100644 images/gift-heart.png create mode 100644 images/gift-hug.png create mode 100644 images/gift-milk.png create mode 100644 images/gift-pot.png create mode 100644 images/gift-rice.png create mode 100644 images/gift-rose.png create mode 100644 images/gift-scarf.png create mode 100644 images/icon-arrow-right-pink.png create mode 100644 images/icon-arrow-right.png create mode 100644 images/icon-back-arrow.png create mode 100644 images/icon-back-arrow2.png create mode 100644 images/icon-back-line.png create mode 100644 images/icon-back.png create mode 100644 images/icon-backpack.png create mode 100644 images/icon-bell.png create mode 100644 images/icon-calendar.png create mode 100644 images/icon-camera.png create mode 100644 images/icon-check.png create mode 100644 images/icon-checkin.png create mode 100644 images/icon-chevron-down.png create mode 100644 images/icon-chevron-left.png create mode 100644 images/icon-chevron-right.png create mode 100644 images/icon-city.png create mode 100644 images/icon-cleaning.png create mode 100644 images/icon-clock.png create mode 100644 images/icon-close.png create mode 100644 images/icon-comment.png create mode 100644 images/icon-companion.png create mode 100644 images/icon-cooperation.png create mode 100644 images/icon-coupon.png create mode 100644 images/icon-edit.png create mode 100644 images/icon-eldercare.png create mode 100644 images/icon-elderly.png create mode 100644 images/icon-emoji-face.png create mode 100644 images/icon-emoji-gray.svg create mode 100644 images/icon-emoji.png create mode 100644 images/icon-empty.png create mode 100644 images/icon-errand.png create mode 100644 images/icon-eye.png create mode 100644 images/icon-feedback.png create mode 100644 images/icon-gift.png create mode 100644 images/icon-grass.png create mode 100644 images/icon-headphones.png create mode 100644 images/icon-heart-filled.png create mode 100644 images/icon-heart-listen.png create mode 100644 images/icon-heart-new.png create mode 100644 images/icon-heart.png create mode 100644 images/icon-heart_.png create mode 100644 images/icon-help.png create mode 100644 images/icon-hospital.png create mode 100644 images/icon-housekeeping.png create mode 100644 images/icon-info.png create mode 100644 images/icon-interest.png create mode 100644 images/icon-keyboard.png create mode 100644 images/icon-legal.png create mode 100644 images/icon-listen.png create mode 100644 images/icon-location-pin.png create mode 100644 images/icon-location.png create mode 100644 images/icon-lock.png create mode 100644 images/icon-logout.png create mode 100644 images/icon-love.png create mode 100644 images/icon-medical.png create mode 100644 images/icon-mic.png create mode 100644 images/icon-more-dot.png create mode 100644 images/icon-more.png create mode 100644 images/icon-notification.png create mode 100644 images/icon-order-complete.png create mode 100644 images/icon-order-paid.png create mode 100644 images/icon-order.png create mode 100644 images/icon-outdoor.png create mode 100644 images/icon-phone-white.png create mode 100644 images/icon-phone.png create mode 100644 images/icon-plus.png create mode 100644 images/icon-promote.png create mode 100644 images/icon-refresh.png create mode 100644 images/icon-search.png create mode 100644 images/icon-send.png create mode 100644 images/icon-settings.png create mode 100644 images/icon-share.png create mode 100644 images/icon-star.png create mode 100644 images/icon-test.png create mode 100644 images/icon-travel.png create mode 100644 images/icon-trending-up.png create mode 100644 images/icon-users.png create mode 100644 images/icon-verified.png create mode 100644 images/icon-vip.png create mode 100644 images/icon-voice.png create mode 100644 images/icon-wallet.png create mode 100644 images/service-arrow-right.png create mode 100644 images/service-clock.png create mode 100644 images/service-notification.png create mode 100644 images/service-search.png create mode 100644 images/service-star.png create mode 100644 images/service-type-custom.png create mode 100644 images/service-type-eldercare.png create mode 100644 images/service-type-housekeeping.png create mode 100644 images/service-type-leisure.png create mode 100644 images/service-type-listen.png create mode 100644 images/service-type-medical.png create mode 100644 images/tab-chat-active.png create mode 100644 images/tab-chat.png create mode 100644 images/tab-companion-active.png create mode 100644 images/tab-companion.png create mode 100644 images/tab-compass-active.png create mode 100644 images/tab-compass.png create mode 100644 images/tab-heart-active.png create mode 100644 images/tab-heart.png create mode 100644 images/tab-listen-active.png create mode 100644 images/tab-listen-new.png create mode 100644 images/tab-listen.png create mode 100644 images/tab-message-active-nodot.png create mode 100644 images/tab-message-active.png create mode 100644 images/tab-message-nodot.png create mode 100644 images/tab-message.png create mode 100644 images/tab-profile-active.png create mode 100644 images/tab-profile.png create mode 100644 images/tab-service-active.png create mode 100644 images/tab-service.png create mode 100644 images/tab-user-active.png create mode 100644 images/tab-user.png create mode 100644 images/wenyu-type-02.png create mode 100644 pages/academy/detail/detail.js create mode 100644 pages/academy/detail/detail.json create mode 100644 pages/academy/detail/detail.wxml create mode 100644 pages/academy/detail/detail.wxss create mode 100644 pages/academy/list/list.js create mode 100644 pages/academy/list/list.json create mode 100644 pages/academy/list/list.wxml create mode 100644 pages/academy/list/list.wxss create mode 100644 pages/activity-detail/activity-detail.js create mode 100644 pages/activity-detail/activity-detail.json create mode 100644 pages/activity-detail/activity-detail.wxml create mode 100644 pages/activity-detail/activity-detail.wxss create mode 100644 pages/agreement/agreement.js create mode 100644 pages/agreement/agreement.json create mode 100644 pages/agreement/agreement.wxml create mode 100644 pages/agreement/agreement.wxss create mode 100644 pages/backpack/backpack.js create mode 100644 pages/backpack/backpack.json create mode 100644 pages/backpack/backpack.wxml create mode 100644 pages/backpack/backpack.wxss create mode 100644 pages/brand/brand.js create mode 100644 pages/brand/brand.json create mode 100644 pages/brand/brand.wxml create mode 100644 pages/brand/brand.wxss create mode 100644 pages/character-detail/character-detail.js create mode 100644 pages/character-detail/character-detail.json create mode 100644 pages/character-detail/character-detail.wxml create mode 100644 pages/character-detail/character-detail.wxss create mode 100644 pages/chat-detail/chat-detail.js create mode 100644 pages/chat-detail/chat-detail.json create mode 100644 pages/chat-detail/chat-detail.wxml create mode 100644 pages/chat-detail/chat-detail.wxss create mode 100644 pages/chat/chat.js create mode 100644 pages/chat/chat.json create mode 100644 pages/chat/chat.wxml create mode 100644 pages/chat/chat.wxss create mode 100644 pages/city-activities/city-activities.js create mode 100644 pages/city-activities/city-activities.json create mode 100644 pages/city-activities/city-activities.wxml create mode 100644 pages/city-activities/city-activities.wxss create mode 100644 pages/city-selector/city-selector.js create mode 100644 pages/city-selector/city-selector.json create mode 100644 pages/city-selector/city-selector.wxml create mode 100644 pages/city-selector/city-selector.wxss create mode 100644 pages/commission/commission.js create mode 100644 pages/commission/commission.json create mode 100644 pages/commission/commission.wxml create mode 100644 pages/commission/commission.wxss create mode 100644 pages/companion-apply/ai.code-workspace create mode 100644 pages/companion-apply/companion-apply.js create mode 100644 pages/companion-apply/companion-apply.json create mode 100644 pages/companion-apply/companion-apply.wxml create mode 100644 pages/companion-apply/companion-apply.wxss create mode 100644 pages/companion-chat/companion-chat.js create mode 100644 pages/companion-chat/companion-chat.json create mode 100644 pages/companion-chat/companion-chat.wxml create mode 100644 pages/companion-chat/companion-chat.wxss create mode 100644 pages/companion-orders/companion-orders.js create mode 100644 pages/companion-orders/companion-orders.json create mode 100644 pages/companion-orders/companion-orders.wxml create mode 100644 pages/companion-orders/companion-orders.wxss create mode 100644 pages/cooperation-applications/cooperation-applications.js create mode 100644 pages/cooperation-applications/cooperation-applications.json create mode 100644 pages/cooperation-applications/cooperation-applications.wxml create mode 100644 pages/cooperation-applications/cooperation-applications.wxss create mode 100644 pages/counselor-detail/counselor-detail.js create mode 100644 pages/counselor-detail/counselor-detail.json create mode 100644 pages/counselor-detail/counselor-detail.wxml create mode 100644 pages/counselor-detail/counselor-detail.wxss create mode 100644 pages/custom/custom.js create mode 100644 pages/custom/custom.json create mode 100644 pages/custom/custom.wxml create mode 100644 pages/custom/custom.wxss create mode 100644 pages/customer-management/customer-management.js create mode 100644 pages/customer-management/customer-management.json create mode 100644 pages/customer-management/customer-management.wxml create mode 100644 pages/customer-management/customer-management.wxss create mode 100644 pages/edit-profile/edit-profile.js create mode 100644 pages/edit-profile/edit-profile.json create mode 100644 pages/edit-profile/edit-profile.wxml create mode 100644 pages/edit-profile/edit-profile.wxss create mode 100644 pages/eldercare-apply/eldercare-apply.js create mode 100644 pages/eldercare-apply/eldercare-apply.json create mode 100644 pages/eldercare-apply/eldercare-apply.wxml create mode 100644 pages/eldercare-apply/eldercare-apply.wxss create mode 100644 pages/eldercare/eldercare.js create mode 100644 pages/eldercare/eldercare.json create mode 100644 pages/eldercare/eldercare.wxml create mode 100644 pages/eldercare/eldercare.wxss create mode 100644 pages/entertainment-apply/entertainment-apply.js create mode 100644 pages/entertainment-apply/entertainment-apply.json create mode 100644 pages/entertainment-apply/entertainment-apply.wxml create mode 100644 pages/entertainment-apply/entertainment-apply.wxss create mode 100644 pages/entertainment/entertainment.js create mode 100644 pages/entertainment/entertainment.json create mode 100644 pages/entertainment/entertainment.wxml create mode 100644 pages/entertainment/entertainment.wxss create mode 100644 pages/gift-detail/gift-detail.js create mode 100644 pages/gift-detail/gift-detail.json create mode 100644 pages/gift-detail/gift-detail.wxml create mode 100644 pages/gift-detail/gift-detail.wxss create mode 100644 pages/gift-exchanges/gift-exchanges.js create mode 100644 pages/gift-exchanges/gift-exchanges.json create mode 100644 pages/gift-exchanges/gift-exchanges.wxml create mode 100644 pages/gift-exchanges/gift-exchanges.wxss create mode 100644 pages/gift-shop/gift-shop.js create mode 100644 pages/gift-shop/gift-shop.json create mode 100644 pages/gift-shop/gift-shop.wxml create mode 100644 pages/gift-shop/gift-shop.wxss create mode 100644 pages/happy-school/happy-school.js create mode 100644 pages/happy-school/happy-school.json create mode 100644 pages/happy-school/happy-school.wxml create mode 100644 pages/happy-school/happy-school.wxss create mode 100644 pages/housekeeping-apply/housekeeping-apply.js create mode 100644 pages/housekeeping-apply/housekeeping-apply.json create mode 100644 pages/housekeeping-apply/housekeeping-apply.wxml create mode 100644 pages/housekeeping-apply/housekeeping-apply.wxss create mode 100644 pages/index/index.js create mode 100644 pages/index/index.json create mode 100644 pages/index/index.wxml create mode 100644 pages/index/index.wxss create mode 100644 pages/interest-partner/interest-partner.js create mode 100644 pages/interest-partner/interest-partner.json create mode 100644 pages/interest-partner/interest-partner.wxml create mode 100644 pages/interest-partner/interest-partner.wxss create mode 100644 pages/invite/invite.js create mode 100644 pages/invite/invite.json create mode 100644 pages/invite/invite.wxml create mode 100644 pages/invite/invite.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/love-transactions/love-transactions.js create mode 100644 pages/love-transactions/love-transactions.json create mode 100644 pages/love-transactions/love-transactions.wxml create mode 100644 pages/love-transactions/love-transactions.wxss create mode 100644 pages/medical-apply/medical-apply.js create mode 100644 pages/medical-apply/medical-apply.json create mode 100644 pages/medical-apply/medical-apply.wxml create mode 100644 pages/medical-apply/medical-apply.wxss create mode 100644 pages/my-activities/my-activities.js create mode 100644 pages/my-activities/my-activities.json create mode 100644 pages/my-activities/my-activities.wxml create mode 100644 pages/my-activities/my-activities.wxss create mode 100644 pages/notices/detail/detail.js create mode 100644 pages/notices/detail/detail.json create mode 100644 pages/notices/detail/detail.wxml create mode 100644 pages/notices/detail/detail.wxss create mode 100644 pages/notices/notices.js create mode 100644 pages/notices/notices.json create mode 100644 pages/notices/notices.wxml create mode 100644 pages/notices/notices.wxss create mode 100644 pages/order-detail/order-detail.js create mode 100644 pages/order-detail/order-detail.json create mode 100644 pages/order-detail/order-detail.wxml create mode 100644 pages/order-detail/order-detail.wxss create mode 100644 pages/orders/orders.js create mode 100644 pages/orders/orders.json create mode 100644 pages/orders/orders.wxml create mode 100644 pages/orders/orders.wxss create mode 100644 pages/outdoor-activities/outdoor-activities.js create mode 100644 pages/outdoor-activities/outdoor-activities.json create mode 100644 pages/outdoor-activities/outdoor-activities.wxml create mode 100644 pages/outdoor-activities/outdoor-activities.wxss create mode 100644 pages/performance/performance.js create mode 100644 pages/performance/performance.json create mode 100644 pages/performance/performance.wxml create mode 100644 pages/performance/performance.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/promote-poster/promote-poster.js create mode 100644 pages/promote-poster/promote-poster.json create mode 100644 pages/promote-poster/promote-poster.wxml create mode 100644 pages/promote-poster/promote-poster.wxss create mode 100644 pages/promote/promote.js create mode 100644 pages/promote/promote.json create mode 100644 pages/promote/promote.wxml create mode 100644 pages/promote/promote.wxss create mode 100644 pages/recharge/recharge.js create mode 100644 pages/recharge/recharge.json create mode 100644 pages/recharge/recharge.wxml create mode 100644 pages/recharge/recharge.wxss create mode 100644 pages/referrals/orders/orders.js create mode 100644 pages/referrals/orders/orders.json create mode 100644 pages/referrals/orders/orders.wxml create mode 100644 pages/referrals/orders/orders.wxss create mode 100644 pages/referrals/referrals.js create mode 100644 pages/referrals/referrals.json create mode 100644 pages/referrals/referrals.wxml create mode 100644 pages/referrals/referrals.wxss create mode 100644 pages/service-provider-detail/service-provider-detail.js create mode 100644 pages/service-provider-detail/service-provider-detail.json create mode 100644 pages/service-provider-detail/service-provider-detail.wxml create mode 100644 pages/service-provider-detail/service-provider-detail.wxss create mode 100644 pages/service/service.js create mode 100644 pages/service/service.json create mode 100644 pages/service/service.wxml create mode 100644 pages/service/service.wxss create mode 100644 pages/settings/settings.js create mode 100644 pages/settings/settings.json create mode 100644 pages/settings/settings.wxml create mode 100644 pages/settings/settings.wxss create mode 100644 pages/singles-party/singles-party.js create mode 100644 pages/singles-party/singles-party.json create mode 100644 pages/singles-party/singles-party.wxml create mode 100644 pages/singles-party/singles-party.wxss create mode 100644 pages/square/square.js create mode 100644 pages/square/square.json create mode 100644 pages/square/square.wxml create mode 100644 pages/square/square.wxss create mode 100644 pages/support/support.js create mode 100644 pages/support/support.json create mode 100644 pages/support/support.wxml create mode 100644 pages/support/support.wxss create mode 100644 pages/team/team.js create mode 100644 pages/team/team.json create mode 100644 pages/team/team.wxml create mode 100644 pages/team/team.wxss create mode 100644 pages/theme-travel-apply/theme-travel-apply.js create mode 100644 pages/theme-travel-apply/theme-travel-apply.json create mode 100644 pages/theme-travel-apply/theme-travel-apply.wxml create mode 100644 pages/theme-travel-apply/theme-travel-apply.wxss create mode 100644 pages/theme-travel/theme-travel.js create mode 100644 pages/theme-travel/theme-travel.json create mode 100644 pages/theme-travel/theme-travel.wxml create mode 100644 pages/theme-travel/theme-travel.wxss create mode 100644 pages/webview/webview.js create mode 100644 pages/webview/webview.json create mode 100644 pages/webview/webview.wxml create mode 100644 pages/webview/webview.wxss create mode 100644 pages/withdraw-records/withdraw-records.js create mode 100644 pages/withdraw-records/withdraw-records.json create mode 100644 pages/withdraw-records/withdraw-records.wxml create mode 100644 pages/withdraw-records/withdraw-records.wxss create mode 100644 pages/withdraw/withdraw.js create mode 100644 pages/withdraw/withdraw.json create mode 100644 pages/withdraw/withdraw.wxml create mode 100644 pages/withdraw/withdraw.wxss create mode 100644 pages/workbench/workbench.js create mode 100644 pages/workbench/workbench.json create mode 100644 pages/workbench/workbench.wxml create mode 100644 pages/workbench/workbench.wxss create mode 100644 project.config.json create mode 100644 project.private.config.json create mode 100644 sitemap.json create mode 100644 subpackages/cooperation/images/icon_back.png create mode 100644 subpackages/cooperation/images/icon_chevron_gray.png create mode 100644 subpackages/cooperation/images/icon_chevron_white.png create mode 100644 subpackages/cooperation/images/icon_eldercare.png create mode 100644 subpackages/cooperation/images/icon_entertainment.png create mode 100644 subpackages/cooperation/images/icon_housekeeping.png create mode 100644 subpackages/cooperation/images/icon_medical.png create mode 100644 subpackages/cooperation/images/icon_orders.png create mode 100644 subpackages/cooperation/pages/cooperation/cooperation.js create mode 100644 subpackages/cooperation/pages/cooperation/cooperation.json create mode 100644 subpackages/cooperation/pages/cooperation/cooperation.wxml create mode 100644 subpackages/cooperation/pages/cooperation/cooperation.wxss create mode 100644 team_page_redesign.md create mode 100644 utils/api.js create mode 100644 utils/assets.js create mode 100644 utils/auth.js create mode 100644 utils/errorHandler.js create mode 100644 utils/imageUrl.js create mode 100644 utils/payment.js create mode 100644 utils/proactiveMessage.js create mode 100644 utils/util.js create mode 100644 utils_new/auth.js create mode 100644 utils_new/payment.js create mode 100644 utils_new/request.js create mode 100644 前端样式修复经验.md create mode 100644 小程序前端开发与后端API对接经验汇总.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..9113f5a --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# 中老年人陪伴咨询小程序 + +基于 React 版本转换的微信小程序,保持样式和功能完全一致。 + +## 项目结构 + +``` +miniprogram/ +├── app.js # 小程序入口 +├── app.json # 全局配置 +├── app.wxss # 全局样式 +├── project.config.json # 项目配置 +├── sitemap.json # 站点地图 +├── images/ # 图标资源 +│ ├── icon-*.svg # 页面图标 +│ └── tab-*.png # TabBar 图标(需手动添加) +└── pages/ + ├── index/ # 首页 - 遇见(卡片滑动匹配) + ├── square/ # 广场 - 社交动态 + ├── chat/ # 消息 - 倾听师列表 + ├── chat-detail/ # 聊天详情 + └── profile/ # 我的 - 个人中心 +``` + +## 功能说明 + +### 1. 首页(遇见) +- 好友故事横向滚动列表 +- 卡片堆叠展示用户资料 +- 左右滑动切换(左滑跳过,右滑喜欢) +- 喜欢、语音、选择操作按钮 +- VIP 解锁弹窗 + +### 2. 广场 +- 社交动态列表 +- 发布动态(文字+图片) +- 点赞、评论功能 +- 图片预览 + +### 3. 消息(倾听师) +- 倾听师列表展示 +- 搜索和筛选功能 +- 倾听师详情页 +- 立即咨询功能 + +### 4. 聊天详情 +- 实时聊天界面 +- 文字/语音输入切换 +- 表情面板 +- 自动回复模拟 + +### 5. 我的 +- 用户信息展示 +- 青草余额和充值 +- 功能菜单(喜欢、访客、礼物、订单) +- 设置和帮助 + +## 使用说明 + +### 1. 导入项目 +1. 打开微信开发者工具 +2. 选择「导入项目」 +3. 选择 `miniprogram` 目录 +4. 填写 AppID(可使用测试号) + +### 2. 添加 TabBar 图标 +TabBar 需要 PNG 格式图标,请参考 `images/README.md` 说明手动添加。 + +### 3. 编译运行 +点击「编译」按钮即可预览小程序。 + +## 技术说明 + +### 样式转换 +- React Tailwind CSS → 小程序 WXSS +- 使用 rpx 单位适配不同屏幕 +- CSS 变量转换为具体颜色值 + +### 组件转换 +- React 组件 → 小程序 Page +- useState → Page data +- useEffect → onLoad/onShow +- onClick → bindtap + +### 动画实现 +- Framer Motion → CSS Animation +- 卡片滑动使用 touch 事件 + transform + +## 注意事项 + +1. 图片资源使用网络图片(Unsplash),需要在小程序后台配置域名白名单 +2. TabBar 图标需要手动添加 PNG 格式文件 +3. 部分功能为模拟实现(如支付、语音识别) + +## 域名白名单配置 + +在小程序后台「开发」→「开发设置」→「服务器域名」中添加: + +``` +request合法域名: +- https://images.unsplash.com + +downloadFile合法域名: +- https://images.unsplash.com +``` + +## 版本信息 + +- 小程序基础库:2.25.0+ +- 转换自 React 版本 +- 保持视觉和功能一致性 diff --git a/app.js b/app.js new file mode 100644 index 0000000..c832138 --- /dev/null +++ b/app.js @@ -0,0 +1,537 @@ +// app.js +const api = require('./utils/api') +const auth = require('./utils/auth') +const config = require('./config/index') +const { preloadAssets } = require('./utils/assets') + +App({ + globalData: { + // 用户信息 + userInfo: null, + userId: null, + isLoggedIn: false, + + // 登录状态检查 + loginCheckComplete: false, + loginCheckCallbacks: [], + + // 余额信息 + flowerBalance: 0, + earnings: 0, + + // 系统信息 + systemInfo: null, + statusBarHeight: 44, + navBarHeight: 44, // 导航内容高度(胶囊按钮区域) + totalNavHeight: 88, // 总导航栏高度(状态栏+导航内容) + navHeight: 88, // 兼容旧代码,等同于 totalNavHeight + + // 审核状态 + auditStatus: 0, // 1 表示开启(审核中),0 表示关闭(正式环境) + + // 配置 + config: config + }, + + async onLaunch(options) { + console.log('小程序启动,options:', options) + + // 获取审核状态(同步等待完成,确保页面能获取到正确的审核状态) + await this.loadAuditStatus() + console.log('审核状态加载完成:', this.globalData.auditStatus) + + const defaultBaseUrl = String(config.API_BASE_URL || '').replace(/\/api$/, '') + if (defaultBaseUrl) { + this.globalData.baseUrl = defaultBaseUrl + if (!wx.getStorageSync('baseUrl')) { + wx.setStorageSync('baseUrl', defaultBaseUrl) + } + } + + // 处理邀请码(爱心值系统) + if (options.query && options.query.invite) { + wx.setStorageSync('inviteCode', options.query.invite) + console.log('保存邀请码:', options.query.invite) + } + + // 处理推荐码(佣金系统) + this.handleReferralCode(options) + + // 获取系统信息 + this.initSystemInfo() + + // 预加载页面素材配置 + await preloadAssets() + + // 检查登录状态(支持持久化登录状态恢复) + this.checkLoginStatus() + }, + + onShow(options) { + console.log('小程序显示,options:', options) + + if (options.query && options.query.invite) { + wx.setStorageSync('inviteCode', options.query.invite) + console.log('保存邀请码:', options.query.invite) + } + + this.handleReferralCode(options) + }, + + /** + * 初始化系统信息 + */ + initSystemInfo() { + try { + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + + // 获取胶囊按钮位置信息 + const menuButton = wx.getMenuButtonBoundingClientRect() + + // 正确计算导航栏高度: + // 导航内容高度 = 胶囊按钮高度 + 上下边距 × 2(上下对称) + // 胶囊按钮上边距 = menuButton.top - statusBarHeight + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + + // 总导航栏高度 = 状态栏高度 + 导航内容高度 + const totalNavHeight = statusBarHeight + navBarHeight + + this.globalData.systemInfo = systemInfo + this.globalData.statusBarHeight = statusBarHeight + this.globalData.navBarHeight = navBarHeight + this.globalData.totalNavHeight = totalNavHeight + this.globalData.navHeight = totalNavHeight // 兼容旧代码 + } catch (e) { + console.error('获取系统信息失败', e) + } + }, + + /** + * 检查登录状态 + * 支持持久化登录状态恢复(7天免登录) + * Requirements: 2.2, 2.3, 2.4 + */ + async checkLoginStatus() { + // 1. 先检查本地登录状态 + if (!auth.isLoggedIn()) { + // 无本地登录状态,清除可能残留的数据 + const cachedUserInfo = wx.getStorageSync(config.STORAGE_KEYS.USER_INFO) + if (cachedUserInfo) { + console.log('无有效Token,清除残留缓存') + auth.clearLoginInfo() + } + this.globalData.isLoggedIn = false + this.globalData.loginCheckComplete = true + this.executeLoginCheckCallbacks() + return + } + + // 2. 有本地登录状态,验证服务端Token + try { + const result = await auth.verifyLogin() + + if (result.valid && result.user) { + // Token有效,恢复登录状态 + this.setUserInfo(result.user) + this.globalData.isLoggedIn = true + + // 获取余额 + this.loadUserBalance() + + console.log('登录状态恢复成功', result.user) + } else { + // Token无效或过期 + if (result.expired) { + console.log('Token已过期,需要重新登录') + } + this.handleTokenInvalid() + } + } catch (err) { + console.log('登录状态验证失败', err) + // 网络错误时保持本地状态,允许离线使用 + const localUser = auth.getLocalUserInfo() + if (localUser) { + this.globalData.userInfo = localUser + this.globalData.userId = localUser.id + this.globalData.isLoggedIn = true + console.log('网络异常,使用本地缓存用户信息') + } else { + this.handleTokenInvalid() + } + } + + // 标记登录检查完成 + this.globalData.loginCheckComplete = true + + // 执行等待中的回调 + this.executeLoginCheckCallbacks() + }, + + /** + * 处理Token无效的情况 + * Requirements: 2.4 + */ + handleTokenInvalid() { + auth.clearLoginInfo() + this.globalData.userInfo = null + this.globalData.userId = null + this.globalData.isLoggedIn = false + this.globalData.flowerBalance = 0 + this.globalData.earnings = 0 + }, + + /** + * 等待登录检查完成 + * @returns {Promise} 登录检查完成后resolve + */ + waitForLoginCheck() { + return new Promise((resolve) => { + if (this.globalData.loginCheckComplete) { + resolve(this.globalData.isLoggedIn) + } else { + this.globalData.loginCheckCallbacks.push(resolve) + } + }) + }, + + /** + * 执行登录检查完成后的回调 + */ + executeLoginCheckCallbacks() { + const callbacks = this.globalData.loginCheckCallbacks + this.globalData.loginCheckCallbacks = [] + callbacks.forEach(callback => { + callback(this.globalData.isLoggedIn) + }) + }, + + /** + * 设置用户信息 + * @param {object} userInfo - 用户信息 + */ + setUserInfo(userInfo) { + this.globalData.userInfo = userInfo + this.globalData.userId = userInfo.id + auth.saveUserInfo(userInfo, null) // 只更新用户信息,不更新token + }, + + /** + * 加载用户余额 + */ + async loadUserBalance() { + try { + const res = await api.user.getBalance() + if (res.success && res.data) { + this.globalData.flowerBalance = res.data.flower_balance || 0 + this.globalData.earnings = res.data.earnings || 0 + } + } catch (err) { + console.error('获取余额失败', err) + } + }, + + /** + * 微信登录 + */ + async wxLogin() { + return new Promise((resolve, reject) => { + wx.login({ + success: async (loginRes) => { + if (loginRes.code) { + try { + const res = await api.auth.wxLogin(loginRes.code) + if (res.success && res.data) { + // 计算过期时间(7天后) + const expiresAt = new Date() + expiresAt.setDate(expiresAt.getDate() + 7) + + // 保存登录信息 + auth.saveUserInfo(res.data.user, res.data.token, expiresAt.toISOString()) + + // 设置全局状态 + this.globalData.userInfo = res.data.user + this.globalData.userId = res.data.user.id + this.globalData.isLoggedIn = true + + // 获取余额 + this.loadUserBalance() + + resolve(res.data) + } else { + reject(res) + } + } catch (err) { + reject(err) + } + } else { + reject({ message: '微信登录失败' }) + } + }, + fail: (err) => { + reject(err) + } + }) + }) + }, + + /** + * 手机号登录 + * @param {string} phone - 手机号 + * @param {string} code - 验证码 + */ + async phoneLogin(phone, code) { + try { + const res = await api.auth.phoneLogin(phone, code) + if (res.success && res.data) { + // 计算过期时间(7天后) + const expiresAt = new Date() + expiresAt.setDate(expiresAt.getDate() + 7) + + // 保存登录信息 + auth.saveUserInfo(res.data.user, res.data.token, expiresAt.toISOString()) + + // 设置全局状态 + this.globalData.userInfo = res.data.user + this.globalData.userId = res.data.user.id + this.globalData.isLoggedIn = true + + // 获取余额 + this.loadUserBalance() + + return res.data + } + throw res + } catch (err) { + throw err + } + }, + + /** + * 微信手机号快速登录 + * @param {string} code - 微信getPhoneNumber返回的code + * Requirements: 1.3, 1.4, 1.5, 2.1 + */ + async wxPhoneLogin(code, loginCode) { + try { + const result = await auth.wxPhoneLogin(code, loginCode) + + if (result.success && result.user) { + // 设置全局状态 + this.globalData.userInfo = result.user + this.globalData.userId = result.user.id + this.globalData.isLoggedIn = true + + // 获取余额 + this.loadUserBalance() + + console.log('微信手机号快速登录成功', result.user) + + return { token: auth.getToken(), user: result.user } + } + throw { message: result.error || '登录失败' } + } catch (err) { + console.error('微信手机号快速登录失败', err) + throw err + } + }, + + /** + * 退出登录 + * Requirements: 2.5 + */ + async logout() { + await auth.logout() + + // 清除全局状态 + this.globalData.userInfo = null + this.globalData.userId = null + this.globalData.isLoggedIn = false + this.globalData.flowerBalance = 0 + this.globalData.earnings = 0 + + console.log('已退出登录,清除所有本地状态') + }, + + /** + * 刷新用户信息 + */ + async refreshUserInfo() { + try { + const res = await api.auth.getCurrentUser() + if (res.success && res.data) { + this.setUserInfo(res.data) + } + } catch (err) { + console.error('刷新用户信息失败', err) + } + }, + + /** + * 检查是否需要登录 + * @param {boolean} showTip - 是否显示提示 + */ + checkNeedLogin(showTip = true) { + if (!this.globalData.isLoggedIn) { + if (showTip) { + wx.showModal({ + title: '提示', + content: '请先登录后再操作', + confirmText: '去登录', + confirmColor: '#b06ab3', + success: (res) => { + if (res.confirm) { + // 跳转到个人中心进行登录 + wx.switchTab({ + url: '/pages/profile/profile' + }) + } + } + }) + } + return true + } + return false + }, + + /** + * 验证当前Token是否有效 + * 用于页面需要确认登录状态时调用 + * Requirements: 2.2, 2.4 + * @returns {Promise} Token是否有效 + */ + async validateToken() { + if (!auth.isLoggedIn()) { + this.globalData.isLoggedIn = false + return false + } + + try { + const result = await auth.verifyLogin() + + if (result.valid && result.user) { + // Token有效,更新用户信息 + this.globalData.userInfo = result.user + this.globalData.userId = result.user.id + this.globalData.isLoggedIn = true + return true + } else { + // Token无效 + this.handleTokenInvalid() + return false + } + } catch (err) { + console.log('Token验证失败', err) + this.handleTokenInvalid() + return false + } + }, + + /** + * 获取登录状态(等待初始化完成) + * 用于页面onLoad时获取准确的登录状态 + * @returns {Promise} 是否已登录 + */ + async getLoginStatus() { + await this.waitForLoginCheck() + return this.globalData.isLoggedIn + }, + + /** + * 加载审核状态 + */ + async loadAuditStatus() { + try { + const res = await api.common.getAuditStatus() + if (res.code === 0 && res.data) { + this.globalData.auditStatus = Number(res.data.auditStatus || 0) + console.log('获取审核状态成功:', this.globalData.auditStatus) + } + } catch (err) { + console.error('获取审核状态失败', err) + // 失败时默认为 0(正式环境),或根据需要设为 1(保守方案) + } + }, + + /** + * 处理推荐码(佣金系统) + * 支持场景: + * 1. 小程序分享:path?referralCode=ABC12345 + * 2. 朋友圈分享:query.referralCode + * 3. 小程序码:scene=r=ABC12345 + */ + handleReferralCode(options) { + let referralCode = null + + if (options.query && options.query.referralCode) { + referralCode = options.query.referralCode + console.log('从query获取推荐码(referralCode):', referralCode) + } else if (options.query && options.query.ref) { + referralCode = options.query.ref + console.log('从query获取推荐码(ref):', referralCode) + } + + if (!referralCode && options.scene) { + referralCode = this.parseSceneReferralCode(options.scene) + if (referralCode) { + console.log('从scene获取推荐码:', referralCode) + } + } + + if (referralCode) { + this.globalData.pendingReferralCode = referralCode + wx.setStorageSync('pendingReferralCode', referralCode) + console.log('推荐码已保存到本地:', referralCode) + + const isLoggedIn = this.globalData.isLoggedIn || !!wx.getStorageSync('auth_token') + if (isLoggedIn) { + console.log('用户已登录,发起即时静默绑定') + const userId = this.globalData.userId || wx.getStorageSync('user_id') + const token = wx.getStorageSync('auth_token') + if (userId && token) { + auth.checkAndBindReferralCode(userId, token) + } + } + } + }, + + /** + * 解析scene参数中的推荐码 + * scene格式:r=ABC12345 + */ + parseSceneReferralCode(scene) { + if (!scene) return null + + try { + const decoded = decodeURIComponent(scene) + const match = decoded.match(/r=([A-Z0-9]+)/) + return match ? match[1] : null + } catch (error) { + console.error('解析scene失败:', error) + return null + } + }, + + /** + * 全局转发配置 + * 用户转发小程序时的默认配置 + */ + onShareAppMessage() { + return { + title: '欢迎来到心伴俱乐部', + desc: '随时可聊 一直陪伴', + path: '/pages/index/index', + imageUrl: '/images/icon-heart-new.png' + } + }, + + /** + * 全局分享到朋友圈配置 + */ + onShareTimeline() { + return { + title: '欢迎来到心伴俱乐部 - 随时可聊 一直陪伴', + imageUrl: '/images/icon-heart-new.png' + } + } +}) diff --git a/app.json b/app.json new file mode 100644 index 0000000..5993e03 --- /dev/null +++ b/app.json @@ -0,0 +1,117 @@ +{ + "pages": [ + "pages/index/index", + "pages/entertainment/entertainment", + "pages/singles-party/singles-party", + "pages/city-selector/city-selector", + "pages/interest-partner/interest-partner", + "pages/city-activities/city-activities", + "pages/activity-detail/activity-detail", + "pages/outdoor-activities/outdoor-activities", + "pages/theme-travel/theme-travel", + "pages/happy-school/happy-school", + "pages/companion-chat/companion-chat", + "pages/service/service", + "pages/service-provider-detail/service-provider-detail", + "pages/chat/chat", + "pages/chat-detail/chat-detail", + "pages/profile/profile", + "pages/login/login", + "pages/recharge/recharge", + "pages/character-detail/character-detail", + "pages/counselor-detail/counselor-detail", + "pages/orders/orders", + "pages/order-detail/order-detail", + "pages/my-activities/my-activities", + "pages/edit-profile/edit-profile", + "pages/customer-management/customer-management", + "pages/withdraw/withdraw", + "pages/commission/commission", + "pages/promote/promote", + "pages/backpack/backpack", + "pages/square/square", + "pages/workbench/workbench", + "pages/companion-apply/companion-apply", + "pages/companion-orders/companion-orders", + "pages/agreement/agreement", + "pages/medical-apply/medical-apply", + "pages/housekeeping-apply/housekeeping-apply", + "pages/eldercare-apply/eldercare-apply", + "pages/entertainment-apply/entertainment-apply", + "pages/invite/invite", + "pages/love-transactions/love-transactions", + "pages/gift-shop/gift-shop", + "pages/gift-detail/gift-detail", + "pages/gift-exchanges/gift-exchanges", + "pages/referrals/referrals", + "pages/referrals/orders/orders", + "pages/withdraw-records/withdraw-records", + "pages/support/support", + "pages/settings/settings", + "pages/team/team", + "pages/performance/performance", + "pages/promote-poster/promote-poster", + "pages/academy/list/list", + "pages/academy/detail/detail", + "pages/webview/webview", + "pages/brand/brand", + "pages/eldercare/eldercare", + "pages/custom/custom", + "pages/notices/notices", + "pages/notices/detail/detail" + ], + "subpackages": [ + { + "root": "subpackages/cooperation", + "pages": [ + "pages/cooperation/cooperation" + ] + } + ], + "__usePrivacyCheck__": false, + "window": { + "navigationBarBackgroundColor": "#E8C3D4", + "navigationBarTitleText": "", + "navigationBarTextStyle": "black", + "backgroundColor": "#E8C3D4", + "backgroundTextStyle": "dark", + "navigationStyle": "custom" + }, + "tabBar": { + "custom": false, + "color": "#a58aa5", + "selectedColor": "#b06ab3", + "backgroundColor": "#ffffff", + "borderStyle": "white", + "list": [ + { + "pagePath": "pages/index/index", + "text": "陪伴" + }, + { + "pagePath": "pages/entertainment/entertainment", + "text": "文娱" + }, + { + "pagePath": "pages/service/service", + "text": "服务" + }, + { + "pagePath": "pages/chat/chat", + "text": "消息" + }, + { + "pagePath": "pages/profile/profile", + "text": "我的" + } + ] + }, + "style": "v2", + "sitemapLocation": "sitemap.json", + "lazyCodeLoading": "requiredComponents", + "permission": { + "scope.writePhotosAlbum": { + "desc": "保存二维码到相册" + } + } +} \ No newline at end of file diff --git a/app.wxss b/app.wxss new file mode 100644 index 0000000..567bfa4 --- /dev/null +++ b/app.wxss @@ -0,0 +1,200 @@ +/* 统一导航栏样式 */ +.unified-header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 194rpx; + background: rgba(255, 255, 255, 0.95); + border-bottom: 2rpx solid #f9fafb; + display: flex; + align-items: flex-end; + justify-content: space-between; + padding: 0 32rpx 20rpx; + z-index: 1000; +} + +.unified-header-left { + display: flex; + align-items: center; + gap: 8rpx; + width: 160rpx; + height: 56rpx; +} + +.unified-header-title { + font-size: 40rpx; + font-weight: bold; + color: #101828; + flex: 1; + text-align: center; +} + +.unified-header-right { + width: 160rpx; + height: 56rpx; +} + +.unified-back-icon { + width: 56rpx; + height: 56rpx; +} + +.unified-back-text { + font-size: 34rpx; + font-weight: bold; + color: #101828; +} + +/* 隐藏原生tabBar边框 */ +.wx-tabbar::before { + display: none !important; +} + +/* 全局样式 */ +page { + --primary: #914584; + --primary-light: #E8C3D4; + --background: #ffffff; + --foreground: #1a1a1a; + --muted: #ececf0; + --muted-foreground: #717182; + --border: rgba(0, 0, 0, 0.1); + --radius: 20rpx; + + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background-color: var(--primary-light); + color: var(--foreground); + font-size: 28rpx; + line-height: 1.5; + width: 100%; + max-width: 100vw; + overflow-x: hidden; + box-sizing: border-box; +} + +/* 全局盒模型 */ +view, text, image, scroll-view, input { + box-sizing: border-box; + max-width: 100%; +} + +.btn-reset { + padding: 0; + margin: 0; + line-height: inherit; + background: transparent; + border-radius: 0; + text-align: center; +} +.btn-reset::after { border: none; } + +.safe-bottom { + padding-bottom: constant(safe-area-inset-bottom); + padding-bottom: env(safe-area-inset-bottom); +} + +/* 通用类 */ +.container { + padding: 30rpx; +} + +.flex { + display: flex; +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.gap-20 { + gap: 20rpx; +} + +.gap-30 { + gap: 30rpx; +} + +.text-center { + text-align: center; +} + +.font-bold { + font-weight: bold; +} + +.rounded-full { + border-radius: 50%; +} + +.shadow { + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1); +} + +/* 卡片样式 */ +.card { + background: #fff; + border-radius: 32rpx; + padding: 30rpx; + box-shadow: 0 8rpx 40rpx rgba(59, 64, 86, 0.15); +} + +/* 按钮样式 */ +.btn-primary { + background: linear-gradient(135deg, #914584 0%, #B378FE 100%); + color: #fff; + border: none; + border-radius: 50rpx; + padding: 24rpx 48rpx; + font-size: 32rpx; + font-weight: bold; +} + +.btn-secondary { + background: #f5f5f5; + color: #333; + border: none; + border-radius: 50rpx; + padding: 24rpx 48rpx; + font-size: 32rpx; +} + +/* 头像样式 */ +.avatar { + width: 120rpx; + height: 120rpx; + border-radius: 50%; + object-fit: cover; +} + +.avatar-sm { + width: 80rpx; + height: 80rpx; +} + +.avatar-lg { + width: 160rpx; + height: 160rpx; +} + +/* 动画 */ +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.animate-pulse { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} diff --git a/components/icon/icon.js b/components/icon/icon.js new file mode 100644 index 0000000..e4d0af9 --- /dev/null +++ b/components/icon/icon.js @@ -0,0 +1,61 @@ +const svgToDataUrl = (svg) => { + const encoded = encodeURIComponent(svg) + .replace(/'/g, '%27') + .replace(/"/g, '%22'); + return `data:image/svg+xml,${encoded}`; +}; + +const ICONS = { + 'chevron-left': '', + 'chevron-right': '', + 'settings': '', + 'crown': '', + 'gift': '', + 'diamond': '', + 'clock': '', + 'wallet': '', + 'sprout': '', + 'banknote': '', + 'shopping-bag': '', + 'users': '', + 'backpack': '', + 'share': '', + 'briefcase': '', + 'headphones': '', + 'package': '', + 'logout': '', + 'copy': '', + 'image': '', + 'share-2': '', + 'scan': '', + 'camera': '', + 'clipboard': '', + 'trending-up': '', + 'map-pin': '', + 'heart': '', + 'heart-filled': '', + 'flame': '', + 'flame-hot': '' +}; + +Component({ + properties: { + name: { type: String, value: '' }, + size: { type: Number, value: 24 }, + color: { type: String, value: '#111827' } + }, + data: { + style: '' + }, + observers: { + 'name,size,color': function(name, size, color) { + const raw = ICONS[name] || ''; + const svg = raw.replace(/CURRENT/g, color || '#111827'); + const url = svg ? svgToDataUrl(svg) : ''; + const s = Number(size || 24); + this.setData({ + style: `width:${s}rpx;height:${s}rpx;${url ? `background-image:url('${url}');` : ''}` + }); + } + } +}); diff --git a/components/icon/icon.json b/components/icon/icon.json new file mode 100644 index 0000000..467ce29 --- /dev/null +++ b/components/icon/icon.json @@ -0,0 +1,3 @@ +{ + "component": true +} diff --git a/components/icon/icon.wxml b/components/icon/icon.wxml new file mode 100644 index 0000000..9f40565 --- /dev/null +++ b/components/icon/icon.wxml @@ -0,0 +1 @@ + diff --git a/components/icon/icon.wxss b/components/icon/icon.wxss new file mode 100644 index 0000000..a8cda1b --- /dev/null +++ b/components/icon/icon.wxss @@ -0,0 +1,6 @@ +.icon { + display: inline-block; + background-repeat: no-repeat; + background-position: center; + background-size: contain; +} diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..0058237 --- /dev/null +++ b/config/index.js @@ -0,0 +1,59 @@ +/** + * 小程序配置文件 + * 包含API地址、环境配置等 + */ + +// 环境配置 +const ENV = { + // 开发环境 + development: { + API_BASE_URL: 'http://localhost:3000/api', + WS_URL: 'ws://localhost:3000', + DEBUG: true, + TEST_MODE: true // 测试模式:充值不走真实支付 + }, + // 测试环境 + staging: { + API_BASE_URL: 'https://test-api.maimanji.com/api', + WS_URL: 'wss://test-api.maimanji.com', + DEBUG: true, + TEST_MODE: true // 测试模式:充值不走真实支付 + }, + // 生产环境 + production: { + API_BASE_URL: 'https://ai-c.maimanji.com/api', + WS_URL: 'wss://ai-c.maimanji.com', + DEBUG: false, + TEST_MODE: false // 关闭测试模式,使用真实微信支付(后端测试支付接口有数据库错误) + } +} + +// 当前环境 - 可根据需要切换 +// development: 本地开发 staging: 测试环境 production: 正式环境 +const CURRENT_ENV = 'production' + +// 导出配置 +const config = { + ...ENV[CURRENT_ENV], + ENV: CURRENT_ENV, + + // 存储键名 + STORAGE_KEYS: { + TOKEN: 'auth_token', + REFRESH_TOKEN: 'refresh_token', + USER_INFO: 'user_info', + USER_ID: 'user_id', + TOKEN_EXPIRY: 'token_expiry' // Token过期时间 + }, + + // 请求超时时间(毫秒) + REQUEST_TIMEOUT: 30000, + + // 分页默认配置 + PAGE_SIZE: 20, + + // 版本号 + VERSION: '1.0.0' +} + +module.exports = config diff --git a/git-push.bat b/git-push.bat new file mode 100644 index 0000000..7181697 --- /dev/null +++ b/git-push.bat @@ -0,0 +1,34 @@ +@echo off +chcp 65001 >nul +echo ======================== +echo Git 提交脚本 +echo ======================== +echo. + +cd /d "%~dp0" + +echo [1/5] 添加所有更改... +git add -A + +echo. +echo [2/5] 提交更改... +git commit -m "feat: 更新小程序代码 - 2026-02-02" + +echo. +echo [3/5] 添加 tag... +git tag -a v1.0.0 -m "Version 1.0.0 - 2026-02-02" + +echo. +echo [4/5] 推送到远程仓库... +echo 请输入密码: zy12345678 +git push https://zhiyun:zy12345678@git.maimanji.com/adminzy/ai-c.git master --force + +echo. +echo [5/5] 推送 tag... +git push https://zhiyun:zy12345678@git.maimanji.com/adminzy/ai-c.git v1.0.0 + +echo. +echo ======================== +echo 完成! +echo ======================== +pause diff --git a/git-push.sh b/git-push.sh new file mode 100644 index 0000000..b59368b --- /dev/null +++ b/git-push.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Git 提交脚本 + +cd "$(dirname "$0")" + +echo "========================" +echo "Git 提交脚本" +echo "========================" +echo "" + +echo "[1/5] 添加所有更改..." +git add -A + +echo "" +echo "[2/5] 提交更改..." +git commit -m "feat: 更新小程序代码 - 2026-02-02" + +echo "" +echo "[3/5] 添加 tag..." +git tag -a v1.0.0 -m "Version 1.0.0 - 2026-02-02" + +echo "" +echo "[4/5] 推送到远程仓库..." +git push https://zhiyun:zy12345678@git.maimanji.com/adminzy/ai-c.git master --force + +echo "" +echo "[5/5] 推送 tag..." +git push https://zhiyun:zy12345678@git.maimanji.com/adminzy/ai-c.git v1.0.0 + +echo "" +echo "========================" +echo "完成!" +echo "========================" diff --git a/images/README.md b/images/README.md new file mode 100644 index 0000000..734c336 --- /dev/null +++ b/images/README.md @@ -0,0 +1,74 @@ +# 图标资源说明 + +✅ 页面图标已生成完成(SVG 格式) + +## TabBar 图标(需要手动添加 PNG 格式,81x81px) + +由于微信小程序 TabBar 只支持 PNG 格式,需要手动创建以下图标: + +- `tab-heart.png` - 遇见(未选中,灰色 #999999) +- `tab-heart-active.png` - 遇见(选中,紫色 #914584) +- `tab-compass.png` - 广场(未选中) +- `tab-compass-active.png` - 广场(选中) +- `tab-message.png` - 消息(未选中) +- `tab-message-active.png` - 消息(选中) +- `tab-user.png` - 我的(未选中) +- `tab-user-active.png` - 我的(选中) + +### 快速生成 TabBar 图标方法: +1. 访问 https://lucide.dev/icons/ +2. 搜索对应图标(heart, compass, message-circle, user) +3. 下载 SVG 后使用在线工具转换为 PNG +4. 推荐工具:https://svgtopng.com/ + +## 页面图标(SVG 格式推荐) +- `icon-bell.svg` - 通知铃铛 +- `icon-chevron-left.svg` - 左箭头 +- `icon-chevron-right.svg` - 右箭头 +- `icon-chevron-down.svg` - 下箭头 +- `icon-heart.svg` - 爱心(空心) +- `icon-heart-filled.svg` - 爱心(实心) +- `icon-voice.svg` - 语音/声波 +- `icon-check.svg` - 勾选 +- `icon-refresh.svg` - 刷新 +- `icon-lock.svg` - 锁 +- `icon-grass.svg` - 青草/植物 +- `icon-plus.svg` - 加号 +- `icon-more.svg` - 更多(三个点) +- `icon-comment.svg` - 评论 +- `icon-headphones.svg` - 耳机 +- `icon-search.svg` - 搜索 +- `icon-mic.svg` - 麦克风 +- `icon-star.svg` - 星星 +- `icon-verified.svg` - 认证标识 +- `icon-phone.svg` - 电话 +- `icon-back.svg` - 返回 +- `icon-keyboard.svg` - 键盘 +- `icon-emoji.svg` - 表情 +- `icon-send.svg` - 发送 +- `icon-settings.svg` - 设置 +- `icon-camera.svg` - 相机 +- `icon-eye.svg` - 眼睛 +- `icon-gift.svg` - 礼物 +- `icon-order.svg` - 订单 +- `icon-help.svg` - 帮助 +- `icon-feedback.svg` - 反馈 +- `icon-info.svg` - 信息 + +## 图标颜色建议 +- 未选中状态:#999999 +- 选中状态:#914584(主题紫色) +- 白色图标:#FFFFFF + +## 获取图标 +可以从以下网站获取免费图标: +1. [Lucide Icons](https://lucide.dev/) - 推荐,与 React 版本一致 +2. [Heroicons](https://heroicons.com/) +3. [Feather Icons](https://feathericons.com/) +4. [阿里巴巴矢量图标库](https://www.iconfont.cn/) + +## 快速生成方法 +1. 访问 https://lucide.dev/icons/ +2. 搜索对应图标名称 +3. 下载 SVG 格式 +4. 修改颜色后保存到此目录 diff --git a/images/btn-consult.png b/images/btn-consult.png new file mode 100644 index 0000000000000000000000000000000000000000..a3240d1914cabad5b558e0f26f1659270baa3114 GIT binary patch literal 1251 zcmV<91RVQ`P)$~P>mfXIhLR1{DF6;$jDcJk|-gRZ1C&BgAd?3ww^-agOS_q@A1 z?=s94D`JS;2$^EHgC>Cs8Kb43Q8)`41<;H~5a^B}41ag-gy8FncPx0BuLAIN=Sx2p zyYqz){CPONr_GOgN}S%ya--fMewe?(d}zfcW1j|%f+=VePC}+ANF?x&2+!-iG~7i- zJtQaWh-aKP@Sj`>@B4Q78r|`8@E(H)dmgurXMLl-Ygg?&+w1V`$Rdqvg!j%EWs7H`#KXg3d3rHcU?rw-eN8euKM6P5|#;> zgtAKE;ncEQOkh0HRwq0Mhn8(KYK3=_46|U=4DX}@Du#Cu0X4%rZo9h#J_5XFJfJAN zLlTe)?=yzr|0Yb+EH>f$Iz;2S<45TG2v&Z}(46eH=r#2$gXqI0+3+0!1$MsR*u>kv z-lDPDL^V~zY5f%Y`2%EMtRgd$!(OtAwfS`^8*CAk*sg5&1tOqC*`%N6aIkiWqGy|y z4T1NJ`DxjZI}x8sq8zE9EbQRy&oitotRwJUST_8^kgH2TM$LN-0eHuQI4m@fdznW$ zT#*iJ=&peH^fFB~T}LfbM|?U7;h`uS{!&1p1Dku52U+o{WvSaNO(vw$Y1K|qSXjp! zdjaX+v$Ekk0tKP0dl-@{&_ipc#Ksw*{=LS+c|V^X&fClffXoPgESuPR00=ZSz4 zJD+-*hA<(%)90fPM2L?QQmahhZT2c7?UVKP7J^(KXs1<&m2&fs}{*HhQ z`0%|>tBg#Lxb3i2;n+-I#nzG?L0t%GP%JU5NR}YZ< zDTNKLh{MeW-E~qn8+;M#>?T^(7Mj~fG|695RMIIE-bMtJsQKulID`ixeVtgftU;WN z<8Y%v%O-Ctpc30b(Yu9Gq(TMKFS7J5IAp@x1{S&mWFA=Z7D@n1;W8~5awP{g_IQa3 zYz4L@f$Z!m4hn~|;V%Ug=r-g{H2ZRuX3eqQ5zulFe@vj=YSWsph(x4>tT~Tbwk{jK zBcM>LTz79`X(|D6DuLwF6=eR%QGwWGobJABgo8`Rp;bRctJXsNNm4evl?e2DpSS-9 zy!GaMmw*c4t&)IB;jL6a#qbs)pk{bWzx`Z5rSKLaK&OE!g`exUwHZ$D-Bu&KrQZ&7 zpRLGnqh3yp@N*g)%o?kk2n?waeokY<>pt56Lnh#Kh^#XBIgRz$eA9fLPAbU+TwU-s zm^HRBX--VW_wJEJw_4!m`fRR${c=FY_bwgJssVm>VD|GX|CgY5LdO}%5V`l@t;eKn z-utsh_aYN;=dgRwsbyWxVRoBL1OJbQl)2BkJ*Tlc&3%?Z>A?PD{S8y4EHhluy?p=x N002ovPDHLkV1i$jQbGU# literal 0 HcmV?d00001 diff --git a/images/btn-text-consult.png b/images/btn-text-consult.png new file mode 100644 index 0000000000000000000000000000000000000000..e072ebf97e7694e037548b0794cea6da620e6598 GIT binary patch literal 13220 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 IW#0$?59D5Mz5oCK literal 0 HcmV?d00001 diff --git a/images/chat-input-emoji.png b/images/chat-input-emoji.png new file mode 100644 index 0000000000000000000000000000000000000000..f5c63c2f83cec778c8884b07adcf571f5e4d19f4 GIT binary patch literal 4117 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 Qw*UYD07*qoM6N<$f)~3&2mk;8 literal 0 HcmV?d00001 diff --git a/images/chat-input-voice.png b/images/chat-input-voice.png new file mode 100644 index 0000000000000000000000000000000000000000..b1279867727fb915e4e489ee6921ca3dab4e87f5 GIT binary patch literal 3431 zcmV-t4VdzYP)jroX_Kq9<_2M9!XQzA80BbJ9YX{2VCN==%F7N{y!?L*o0 zsgWuHeMys4VriZV4=jC2q(s6fq@)rbMh!`-6n0Z8phRK~Yw&{Yxw&V|EMD*2*|9xi z-#g4t+IaWQ?8fq+bMCq4-gAb4515*oN`>R`hxJ-5OKK!b2~7co47-%6-$7FZAl3YP z6v!k2T4b+9Ahbw;6m<5g5K^el%$!N5(?yUR0unM^E^pE)%@PVZpd`yg%76z@0+Z~e zz+UqxdO}r?f@DY`Fj}Y*jSklZg`AZ&b^{8)EFJPd_3>~hlvh>tOAs7k1vW)hbQoek z=0HH0YtIAJ#%h(yVNu<=Z~~+Ig&G;6ppW#*Qrm%vJ*GrLd$dU83of=q^9i$0qD8Fei61ABS542Xs?CtnZSM(wiy(fC}gBp5qQ@HX2DaYuP?fr{hx>6 z9s=j+a0o`b9eHT+jSjAWeT3$1(PXsRMWJ*&?UMeuwk{~=sxhdJ9Y}X?XW^} z-LPf_JO-^c6po&TK-^ha!X{I#>4j3Mv>iOgGYdBI!{O*Imi^fQ0emRzNhJDrf=75F zFuLSWI6O{3Sq%XT;-$-WU0~*RR@Zo;y}(Lu=v;*Idegxgu~NuKTzpa>oJ(;(CW0B6lP(&KHD|aR~4XO6X8B z9zP6i(zd|$rBL+ry_BeNR6zV#KYUS2ot4D)cw%}6f=I%h;o{(kRth=B^S`rvH4uafr4AE;KWPxoljwj>&TkY<#6?nQ* z*#x&Ld60znkNg8ZU|*fEd2l^EGq?eyAw+U^M{;e0EdADPAx`Hz!LZy8#YH|iavVNn ze@hl<4My#*a|OO$E)UC+(7-tt-LhNYKNK?{6CDCHXA}`|uXB(o9eH5~r7Ljk<5O_*^e5cEiTA(qljlJOupTO@se@M6ECojEB{ez* z5`dQWhuv>mUQ4Hw@bbtDa36cM;woD@y}j>maFt`Nqrd$*s4V!B0?u4)>7q#uMnDqI ze*PcsH9K*U`%+2x>9*&Mb+Hx4j-LQo;LXMJ1fD9Dvh4O8NW$6A&smPI9eCVUToV*| z!@6%MY9PFX#uAo_bjq@&Jb|x+Oelm5qlRRa4d^xm%Y}v;N9G7DQ*aev zjSgf^-Sw=%xTzpha1~$;23w78dRAcOo;N{&FKBervjUSUxu;qx_!*ttt;N1O2NOL9 z0lt9HyHl+pu(2%|9LMnyBxNQ&41r0dl7#?Y!S)giO$~u*P02xkuQ2CfrT#;000R6* z-GYs-u0f@O@4$`$hPHj07BounDcA+S4+AU;Vu6R}KKl5S@%@_YN_g(sr$I!7f1kH& z**;21%IIY-2!DU~2z=CV=|+#`n9e|eFJaoIJZ~o)&`U@l zu-^#;_Bp}{%&RgebCw|Yaf-IjPdY!ee(93++3FalY^6NYTJ3Q!L4k#OBpa_5h_}W} zrK|-$H{rbP@`wMlE*D+$!2Oov#VPCa(&^N~l@#4Zp@aw}@Z!bR2iDJV+A%E4eA99q zvr0}%K=B@4W#Y=mR#=W-E%F2ws|5r$85hn)rxsa%NS7}E-*SB8dh5C(wD>p24%mu} z*WP0H@Oa{}M=ZzB{`(8d@dp<>ucqh*EXh%1>tN#yV)0sv<8miBMQKYp@1s{x3fKAwz@$jqg@#)^ zG=CiHgfMWja>+l~`d#?b8-L~2DV;cd*7$M?7L7kM_@w1E?A*iqNAVzM&h8;a1>5m} zK-gL2$C33JaW;esq$P;}6RdfqLv>e1zv<7kDv zIThOt{v!>pZne*Y|imF?BCDxyZEfgh&BHric%{mP^nCUV4$1kR^3ltux)dMmi&uX zUt<4A9tWpj!<8Mc4s$IR!P6T&>$MBWdP%ja6^te<*Kd?3MB7!uY|-nx|6&<9VTS3I zZ7&$}o*lSe{q(ahj5ZIr%qo}b$r4TiB^(wm&@%b)i$mO?3B}#R z*1uk~9>h8K&3y;C;$l$tGd4|#3IA1>~O0kFuLS}EK)zW zKr7;xUfjZ#bB}g(xi$bp%c!IkL!2_f76NOK>k#Zx#&buae z=3o4FR$v*ry0_9U7%h1@6L}@uFi5iv49s@L)cRH=c7n(KKJ}MD=-!+^@XETAUL?`aB%#+mK%O|ijfFmNt88N=j-cUp~ckP1|o*U4+VYg~Z?E0zO0>qYDj zz6Vw;Je1b9*bB85In8LTUb!VHu1Bq%)5=W-J?T`M9$D~XaoX0h0@TrcEv?HYQmC? zMh}w^8G!&#AP^ZzCZiptiraS4jg-peVOpbO5a1dpuBP?vhYoQ?U?dRPJ=_u(ajj35 zrmFkdoQ(A_LAI-1SU^Sf&{ns^MO?FBlc{n!6RPNWq6E)kY6V;hMI!1@Btl$misQP# zNFZ{HPlgW4{NNYNPr5~BNIWNRTJndDfY1&SIHyEAs!D;%3z@j@ilo4J8Q0>#-#uN002ov JPDHLkV1kN2ai9PI literal 0 HcmV?d00001 diff --git a/images/default-avatar.svg b/images/default-avatar.svg new file mode 100644 index 0000000..f7a6be7 --- /dev/null +++ b/images/default-avatar.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/images/figma-gift-shop-chevron-right.svg b/images/figma-gift-shop-chevron-right.svg new file mode 100644 index 0000000..0efbaa0 --- /dev/null +++ b/images/figma-gift-shop-chevron-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/images/figma-gift-shop-decor-icon.svg b/images/figma-gift-shop-decor-icon.svg new file mode 100644 index 0000000..ab065be --- /dev/null +++ b/images/figma-gift-shop-decor-icon.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/figma-gift-shop-love-icon.svg b/images/figma-gift-shop-love-icon.svg new file mode 100644 index 0000000..9bcc89d --- /dev/null +++ b/images/figma-gift-shop-love-icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/images/figma-gift-shop-price-icon.svg b/images/figma-gift-shop-price-icon.svg new file mode 100644 index 0000000..e3d727a --- /dev/null +++ b/images/figma-gift-shop-price-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/images/figma-gift-shop-section-icon.svg b/images/figma-gift-shop-section-icon.svg new file mode 100644 index 0000000..8d93f24 --- /dev/null +++ b/images/figma-gift-shop-section-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/fuw-aixin.png b/images/fuw-aixin.png new file mode 100644 index 0000000000000000000000000000000000000000..5a3934d86ddd9e977ea12fdbea96e73040636186 GIT binary patch literal 13206 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 IM6N<$g4tu8=Kufz literal 0 HcmV?d00001 diff --git a/images/fuw-dingzhi.png b/images/fuw-dingzhi.png new file mode 100644 index 0000000000000000000000000000000000000000..7305f13a45fb27948e71692f0a36d8885fdb1ec2 GIT binary patch literal 12922 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∨bS#S_eHmEUU>(&=$C@b73 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 literal 0 HcmV?d00001 diff --git a/images/fuw-kangyang.png b/images/fuw-kangyang.png new file mode 100644 index 0000000000000000000000000000000000000000..fae8c4face6f2cd1977485064f9161674ffe22fb GIT binary patch literal 12182 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 Y1~M_X#cVIHB>(^b07*qoM6N<$g8d|jivR!s literal 0 HcmV?d00001 diff --git a/images/fuw-pinpai.png b/images/fuw-pinpai.png new file mode 100644 index 0000000000000000000000000000000000000000..f06c0eec90c32309329ec8b7b03dd01fce3d2d9f GIT binary patch literal 12750 zcmV;h+H$dDmJh71|{K%_DZ1Sq%ITQ+4GYeZ4R z$C+(rMZ+#CImYhmJwYF$GL(#rz(oKqDoajFIG?m;SxBi=FEQ=;j+~a55$|&s)ra}# zdFzS&88U)TyFXt;WjHh;5|at0 zlH_vhvQM^Ia>=$3Df!&UzrIS6HY&DVo+Uu`(8rYFP=WvAQzW9Br;%19Wf3AOyPQ)+ zNE_PbOJRNAORNVZup^j9qQp9+sVYuP-jC%v@0=*@T)B@&YNyJ01WhAtP*+wNsvh#fRA0sz ztw+AZv}B8XQYlMDySJa(l z@byq3TZly1x4CuI(Ob5x{_Kt&>#l9v{>C?U?b!G)?K?L-r>9MAdfL^tF%C`G*S>T8 zA8_ohof{sVjH=bd*}o}vODi0VU1tAIYAW|XEFoi&Y-@HOH;m>_{T{CVyU zqUe}@k%}ik=Co{1XkE8z#@LBj-IJCC*M=fN|_{ER7x17#3%1W=B%K@_)J)hKG2z>c{6INm_sns>gLuYGChgoz)R zHm|9obk2B`KEzf8P9&n;>&*>+g&;8Ly1Tz|_0=~nhf0BM`0x0yix)4>+yCOqK(3cke!Q*f)ry)R zuwT%=ktX`AB`ODD#Qf%)Z;ttc2a!|C z**B1@lEl?Y;LvmV`Lssz`mQjM!hrUz&Komo^qoxHhK2^!)YLfQ@@pZLA(0bA)}D*@ zz3sQonDNn8Mdc@63zU~gU>`Aw_K*rvEvUA)nE2v_q_iQZ>-u=hL$?OhiiFz5WcE}F79Lea;+(1;_|;Y9lNY+1BF}?J zUaK;g33~2_W}UP`$lF8QQEK$)QHMmU1{k9FQ1PGT|K{rJZqiQIJ`=;^7~ z*wZ@mt$%yc6ie zkvHq*=9{g@brcSC(rxv%-e52_OlN> z@YE)%^+XRg(K}KV{JOOAYhqwn(t#KR!ixaP&9CLb|nDN-D9Xj4;@ z2?1vqI1t5yG`85Zb^DfcXU+Q9CcAFD6EO0;G)+Hx=iVs^qlyr@k~(lCZW!h%S+pTf zgIk8=W%9zZ&d57>ATmjM@ywRVQAflvyB zpobJgF2=l+M-?#G*!$oh(KR8js0|r;hYr3ZhOrEfk9M&cp5UFJW|ViiaN$D7pSY(b zI2N79q9V_%h8V2ai#03z{C^3fO`>Z!b=dI~?g&)h$i+y?thk#?}(2p=W(H8(eB23v)}-U*w{ z_K$W~tOBY5sS{$upea*69^yu$Lq*^MUDr>Aq(7Xq$s|sd*tTWeHI0pRcVId5^9~=J zpBT%XU0-amVQ0~W?P0-G%YeseNC-S6DnuX|+ShsIpBeI^T0)`dDb_~YOikV!af~FN zXBZx!>?H}PsiFQ3xH1(*OHpfj&A1ZpH(=nEU0{}-z_!Xr=sn_M@nPfd=1;*uxcch9 zoCu>$+1!SVyu%L)(NC~OT|uWVivpd)0QAm>EcLQtDuf{L#Tp`OLqYA>zWxp*qT_gS zu4fo#uzM}v-Sy3hljmHI+EUsa+JI9`4Mt_%1vQ3>z(b)zh#Msik=Or9Q2-$C=FMx) zl2#PZdo%J@52U*Rvs_ny&E~gP&bLWFuHme*T7#d-rmR}wtB_@THdSVjFE9~qB=SaV zu;~JbUVio`6Pg>FzK?b2y;(|R^#Sihy3ranX3YIJ-+bi+)SOtCP%|ObbyjYdT3HAj zA}xm)F-LQ5Ib6=O1rQx|;)lL0k+w|ltzM8mJag`er(A=YlUsBslCEOJF=6B0{@7Hx zP!@5{5g3lRvD++gU$brFnipqI`9RACENA}S>Ib*#(4ppIZKxUVGOs*$8X0%SO=|n3 z;L8n~$%B@_k_KtBkRl2xrd1aSJ!sMqEw`Ho(b6{~Z}kJJgd|c;nbh(<)#FZcxe|ca zPlqfEsf8nD5yOKfFgg32(T2$;KpV39K0-jN4{Gb?*Zw*esRdYv^vzHkfGPp#*E|1h zZP3Av5q%i&x;rq3(!Q7sxfC6-%U!C9z0c$%P?xVo;K}90ZQiwsAH;GMbPSBKx;)Uj5&j znnvA^m9b;TWNxz>1IP;!1N+~9|J;i&{rhEDo-$=hEL|S3cY@t(Qd5&&JYW(JN`>&% z7|u$PuTOe{vE+4CO=DxT3m0ePtv$e{CT_Ln(W7rfjk;&g9tf*Qnu3M={(wkfQYQwC z!7?XlQqq@&^+m~M@a|aXP2!-?(1eT^XT^bbz^s=2ZDCDwZ9(#7!Jdt23tS5}(|%Oe$#z~UD^?IUM=S-)dP zN(e9R!_I8fur5}!K3E|XV3fUv42~l9)0X_v&=YR z`c(CWFDy{=&pKUApWdQI5-9P#{hV_@-?C!Gih{HOt5$-{Id~^T@QK^iKfYA-o4~Qx ztq?T}N?c36I%F-OI)M~^$%dwc%(-rf7o`@oF3Fy;~@_F8e6TH5mkb0?1_V;~F)+NQ4KbIh8+}k5c zWhJl*C+0X%<4nLPN2i{8syXIYOBZWpFrEPN*0KRNSmOFP^^}v;#TS1@ojw1*s9FUj z`LvIpcx;OnS}|-Miv)=!@jVcYi&Q^JTrz=4Wr&JMUjLd-q1V3OvufG*3)SF>C!bbN zJn@WLzu`ZM9y@N_7TeOYxLw5 zE41Xk|KU_3Q`FM zI%{FO>fuIgg<8ywK+R9GLzI5^Gu&U9?RU~wV9;5AA2T%fcI(Pdk2`G452R2w$-fvdaeYAA$l~E5P8}1-}b4tRI9d$o6O6X?x?u=z2}>aqu$O?T%T%1@1^BcAYujFDI`9)`(ND zzy7*aJJf_yAEw+G7tpyS;bf!%wasCNjaAivUSSo_RE5&h>A<$VoLRO5#z;WJ7nJGu z>SI5D8RlZ-_cgHkj0q7+_*mR$kiy7+FeeWKfv0Qfxe|fpOPwV^Dgvi#ufY?Ofi0HsF>XBWBp%-%(WfpPqdlW$^{~@I@9=kr=M*#xA z_`;I)An|bdfbY2TJ`=P^L#-y2m+I60&j0(TiW{et)YCfQ@L9954eBJC8c~i+NdJvf zvK;;69EwU{3hl9rBXsqMrr5j23^k1Ci=6PSe)=<)n>5C9C8!T2@QiQ^q85R~Eu{%l zrQs)QX+h>{Wy%p27~a?>XbE}=>Ea>{KTGjKMC95?bmpfh-+||lyjtSoi_7UKoC(Ve zXB4hKfea+cTvP=SI3ELT1u`5Gm^(bt(KzY!E`}Om16*busstK%QY&^z7W2~iwy6MJii>emV zIs#QSjHF5$d~BG(*epg~3C1k>@dKt^U9|z~0{~9!-@iW*g;T`^sOtcwSTLj51){{2 zNPA${8F!cJooD7+0-Kh8)gXaGC^cCI7;1xWu*pWEA2Wtq0-ye|Pd90os#z98jt(sv z&>*!nRS*{Q1B*pyU`gvThNK0Cvbv12`moLath~=`fB*?Ftyr;2-KV8qv1Az-e3(q$ zgg&QSN#&Jn1yV`C3!%yD@_@yVvpu>?%M3-G|Aj_50#a-9sL0KufHIwyw4N~NKw?{6 zs9rZ(^*5ytNw8uVtid-B!>pJ@triI^DU~4b86RITTO_bbN?O;vh&zMi$gbCUPVio- zjFJelqP9xPI+~SAbljInf8#u0#^C!Q93DW&OsmeN?=xvdTndrJFiXo_7zxPd-OFyb z5|{^oNdPUDwL`!x6Oo z*YtBB+(XG6$E^sBBhk4QF@0K#se+*=_hs;b$cX%556Ktj$9Q_H%rQi-PZf>a|eSL7iIPLOb)T{hb= zsE49NcKCm-?+^31yF1@%{)~35j1+)q_kR1I`%EhD3y`!hvT&`DLS8^>TTLPK3&BFo z*-Fx9i0wmI(J?$gxZ?E?iUl%w&_eSgVLwbV0u631qs;?IqqJ71Q6oA#I$HT*MiLMh z;noqQ_=jt;6-a*}?|R>Xj#d6X&%ieaa%*<(qM@&3cAzgEST5lAm||22X~-$$wLKED zTTQ*QdB@5*hmDhnO3iiZR|p27-SfAy=UjgDmvdb5o?X+`SceVXdS0(WT{5csEWF!Ja zKH{ROMo)&phSm@u>-6c-bO$ zlyMbqrP0;ZW9#JOJk$q6Aj<<1EK z+O=y}PQE;Cz%93PRn0(uzCIp%Y>65!z_08BA4D49H|BP6(noP6A@8`eqZ0%+EF=l+ zy4+RdS)i5~oR!vdP#-F1UlN!Tc=FbL=A^KU0VX#_t|ydqOxpKnBryDdU7g)4`Oj4= zBqfnyB5n6SqACsC)6YVKyR9#2j%>S!AN;17uv}?~2cqL($8y^2TxfttVzYmdHy}Lc zpcg|)EiwS~IB7d!69bNi5^%!^05+9U*rwzIT})CB6`&%@`(6I)Wy`hjiXp%R5NuE}sYwxcM`l2QvY6ay14^e1 z?w4Vb?9I%69tV!k1CcvAI?VFRzx&xbgp{kLgTG?9<<>h>TW<_iN@ZW6W~c+hAyNAS z?%?{vAHDE23St0$iwv{Po4ejBDV$uix5|m8BoZh_U_%Q z-hFqg+Pry-A#5oPGHXHd0x=EpvZJm2{7-!1;$>O_D=WavA<9N9>XMb93loR+UZo2Whni+bl zv$InjH~{rQwN5zv*g1A{Op{Vck5$YL0bj~WDXiqG{cOd!HxxXeX7vto z5hk&CA1JNT94e=%WD5-wU)0Kdgg>}*+q&oL>g&!tc(7wcO5sBZc!xk|N7qA_Uiu|R zUevgdz7jr000VPY9sDSqcxvr3OX1Y}m+VGMY37kwriXJ9R}y=yPGpYTu;I67wP>F( zmf;f~uHgqV3=TY?%Z}~a&p+q9i3Qjl&k|&6X%F- zR1kfmBxM{h%)cI90o6(xy2NvmXE9VNIYZX|7;WL^45;~rYHDiLqlzT-CRhXq?pF0~9jfb<1< zfxY_p0{228#4LG%hzEG#Nf4FLjEK>ncy8p0cRAm8uNBO#C&H~m7uHpc(o*=B zkisL?BQcWUq;IUH@8PpAP(6Z4L4^?Y0ky6*;v|e5*+C9Nh8aulqJ|rzWaTqJUV&TT z%zE#kpwveanE`o$_VzZ@YX>aZk<@B=qyxki>AQW$&f8F<3VBseZHrwok=Ln~lzO22 z8a$3&(5?IuI4&IU+eu#PC!)ZxQxwF&7m4&r_=B;2^yos9(emh`Al|UOg@l3E4spw0z zNj)miTV*W%LM?^kCx5o=9>mdcJv-8QhAKo)zC(=OC*NwBcA^nF@*YvMB6-UuaK#jP zGntA~Q$xT-7K?iOoEE9oZwcHlw(~imu(E%Q3V0(hw|cPIZk2zvVeL!vTc%C=IW~@j zM4yoc)Q%E$du{FO$DMJ;r(0pXfxtG;CE?@s=D}^A`oPk6NOa)DgLv)s9Xx5Qc<^sj zEtn+D5?M>4PRu&)wB?-#JMY1Arb<*7pgIV7p*n!Pic~>Pn^ji&`j?P>_z`uJQza=) zVQ6$$kay9utXf5N zklmqkq}`h;^frk48GHALOAQ_S7>EuW`f6NjQc~7kYi0YVVms85`9o0lv0d^;fvc{% zw*3#Q-uS$}-_9Ky6Tzu%n2?(i0Q5A+=VetEdGY2$dvy$ry+Z z><@nEh8-Y(uzzjNi*Wm-7k5PFfKWa~?4-$1l%_?p)@jh_lO+Z>8hwdjWF#3Z55!gS^3oSd)I2!(aBCqq4 z0t0aKyQ{ydBRT&E%e62-%t(Xu@c?KN-_|ni_^+!TUA7?9>$keVoK;5}Ptdtw#r@i! z#GMcZN?>y?>!`jrf|4{M?mw81QN3AeSk8%0QHLob1UYtYTmNr$^|?>s+KAF+X_u7& zC#iRKb?9i@@mK1osbcA?;k*y_9pz?rbd1JMU zX!4?8XP=Y?`rYMild+f^Ytrypd`==j(ce}V#V{xqup3P!|6syR0?1$<)Gq*MDj8Uv z3c8l_A%e*B&-?7AmtR?Z0i=Qybgd5CRgDmxS0wN6SFF0wN?rp95rTOy9(jWiv{2HH zyGWUQ{DT!^F;xTXO-OVZBz;~{rkdzZ<{0FS@-^A5W{-`}(<=?j#j9!`y6qb~{G zCu3>=m`h7lk;G!rYCS8@5jtf*jD?q7-P%K9(|x^h^T1b`VPHVnN%9m7wjVwIqtiiN z|2XL?N>8q3jRd8*6Gu6obln1i_NXY%<1lDrrd*1o`ywg8A%7&HJK1sN%!Av(VG^-( zaf(acFkc_)#DBfys<#&Z@W~6hyYfH9HaOZPT9lzQNY{qy*M2x8}c;l+PP!r z#g@FrP=mxpGcO(WbngLCHmD0U&YQEIsi(PJU?R>Q$%10Kq!+;w+Ob36Z+C z`+uYFYEQ%IM;-f7qlRhH8gCDzE(|tKu)=%Tzkk0}_v~>7~_ve3c109x7**x+xB+E>su#i{MXCtqZ5~+P7(SyZhR&E=m>;z_OlMt z)H`URnR6ltwPrPQX|lq@Hc5*B@mWF|&pPgm=O<2@b-b?jjBY2@=b_@Mke~$Xx+n{{ z$mYyBL&k3NZ4I;Tk=K>K9f38mO;Q7(4vGq4_1-vC&O=r9Lm`0`IV1|S32wPT+t^2J zq%jIi{KSyPu!3Pw_^mipPqeN;x_|hi7k=$`FRwaYSD$9UHFRMGi8NA)%*2%%r#s*M z{7S9sqAawFtX+(ip_mtkYIv-tex_$Q>Gqw6{=1#@`T9ngN_8%B8|BC z&&M5q)`dC@-->N06FrOT>gr;fXA*OZVg!$reQ%Oo2C(YD_vJw@l~R7obVF9;{9+nP zfl&#kX+hVl^mQhlgF}&a*=)zK1=bQa5dW~8M}AAUjqg8o>eOh>s{r2?Ji_e-mW%8xkLy;I)7Og&drs^Xz$_#l6Agy0>fVexh z@3>9t|1aQB`Cze6$SaqTlt?kyxF28YgwdP}5?j)md8(L1Ekhnd)h3uu~(=8}-XT2g7F4FQ{41WkP3bqcmwp>-lIq#~>AE{?t>8 zkKeXs?L%!lHni?)+Zgk-clVp|fqj39yE@*D^Ie;VWx~Dj40sm*`Pz4GY}anX+wQ#c zYah^r9V7n{%l7|`(&RP&A7vut!excB)1);Mm2Cr;xqTNoncH_{Ci@<9-n%S_stzS8 zCT8NohM>|2)b!e7M>=vT>LdB^WXd7UFyUX>~>FbX0C>-2>;NiACyML-Z3Pw!`T}z~{ z7;9qW%IHDlF7p%83q}&+aV+AvXy+pwb~D$kL@idydeViNQ<_m15KVOAB5msYb+=ve zC$Q`h*?k~TCu~^ST}P8zEqmsNXHJ|r@l%bX8b8kZa^gV!AB4y=BBe6>KjhV)z!__k33!t0ZdLu+`X!^qjUA1_xCMdw(QyGwfk$A z?AOm|$YGz;h0_txkq^{sd}(s4M~@zzrylYZCFVF?m;Aiq0TNG9f{ zH>w8k(o0L{jvv!Be%$!+bL;A&@%8nMb8~fSy#5bn>bX!fga5Rav8`sFk9X=`)l%XkKGSU)8n{TkOA#M$Gb;v zmr1T9e&Gq%n=he73Wf+q(m6r86f!#LuE@>j9m^6nQA6Naz2|N6CP!HQ?_4HQMpSk} z+?*$cP1zH9eYH;B(;<4U5-Ce) zmeNeX02vENEXRbd$?N39ZBm75LX%3WONoz_q;7%4RaOWPfpavENM-QfM<#iFkSc-I zphRW|y3H|lda)M(Nl0SS`+W7nC$dj!M;5QD7D6|BBzB}D9om=R99S{LW&(4-!N0#3 zfH0H)U%Y@sy(%QGA0%>;m!vG$b|F0n^>J- zR}Z_~*xe>%mN~iJ%$$i3AhBO%GqFNsP8>u2*aS(%qrj?w+`E{Xz`k5hRZ6H)>^rCB z5Q(L{UD=ZW;RLJ<{Y6T3mhe)LzMLZ;a>C`N3bWfn*=`|!vLg+X3zIJ)eVtm!X^$)T zC^*pI>9+;aRi)^-tSW>IG_I|Pz^vy6g##0~nlV%@GRg)Uo^-KyMa}Nw-X{C8B*@z= zH?v=Q20~Jb>J5jwe!Kw5IVQLUjG-1FvK`a-@#FJoy@1KfFEHm|KjyYkYGj*5xns~L zoZLf>l`wDUd1Tp6W~a?r@Sii5a^WWLoLR=@=!2TTXRd}a!#5@(e_19!l0;KNH}s== zka}sxpQs&0B%|*QaDG_Uv%PE72d+~e2sRY>aryauUDsEuY}w^J$=a8SmZ4nmgn=Ki zGuu?)CHMp;!X^kz246_EFx!~iMTnKC+Cy_*W+Kz|68gFds0abK80)wG2V9`j(lM?) zYe{veb%d;Cw>>Qk9Wead%l9R+PZkbct3q8mlvfavm}zdV0;!tj22+8kq)JL!|Nlmw z5$1o~zS}ZG1(A@#MSPeA9t%|%_vb1&K^W!vQH9(>vXIZ&cWJ32BoBBIg7*&WOZ+dE zx}mZVi5u{_XBaNf%TRvCluDO2iEwEsjOu5=DNp4{IVM!S`$2@@$DqPewrKD{d9{in z&q5JDx(g6xdT#Ziz$HX4OQcYU%)WC!G=&K0)Dd4bQ7I~lN_X=G)q%h>C5Racy(^Uk zB5Df0D$$2ZAxSB|M9O(&C?J%q2hI6&yQO`OsH?sG%x>=KtOZrSf4nN95~OX*ya^*0{tHecg;D|IN2#EHIYGLVeZ{1! z5SyPTu^du`iqs^Tls&{ylo-WS4V9u-t0EFo6@Dm6(MJ)wF9#gxc(;Bis|GohL3+|O zD>Q;+L1%SB8Z`ZvJPei0(~!;)LY?NtfvVko6t>Td-l~DR9N_EIes`ZkqB(mCvg)P? zM6#vyw3F61LZY`C+F!8D`}sa`vd@gz`zSIHG6`G+UQaWzKz6HM2`KAwtUHIV=MkBu zZQ>l3sJh+H$dDmJh71`pWT;~NCyp9V U1xqWpuK)l507*qoM6N<$f^74*OaK4? literal 0 HcmV?d00001 diff --git a/images/fuw-shangcheng.png b/images/fuw-shangcheng.png new file mode 100644 index 0000000000000000000000000000000000000000..46a4e9ff93bbe3ce74d111df82692ab549de7252 GIT binary patch literal 12535 zcmV0twKO1S=s4c@R@cAXQ1K>h%70?SI#I{m4V&NB!9OL&5pI{F$CF({=;2HoIjVGrkoF=VX7E&5BOiX`%Ag3p0 z!u!HS?P2;nwVv5uqC^ad$$rq8bx&rQ3u)z;+`8Hp&hy(7Ugu-6y{oIMqOKM0&(|;| zrY1yUGQmuiTwz`I$u>_e*%l)upPT6Gn=EOQTHEDWB4m$!OeLlY^oy?|5yQM1X+=`j zAfoZh6;p$>v29uk^Ya+`VAMxgVv-|DK$DlYRS6^K`E&K;$V&GJ$6bB()h%>Cx1BzY z3LTq~x_WNnq&Ja|pu}VVRj)b8rBwi$XUHXlRg$_w^9(WhJd-CmZ_k}$hChJJIU`7E zWAelOH5C;M3c&RGWzW;bWT{Nb)GcoTdK_Ub!$rRTHXxWpDP1XqBy8#`74#c zkm`_Va%eqN$QC0J_HA3Y?zHV2Hhg&3=FOkq{rc-)+p~G|Lp_@}KijiqOZRR&?b*6D zjZN5Rk6Vpn_iWknW1PF&USr$(^&i;s>Z=!+$OmFZkkTfi&sQ@_s~DX}G$zB>i2vY% z1mf`Lc{qroWA;TFo&cHCvO8hpi!UxXs%CKl(5?zp*WHW9EdxL;~nI!sq@|Z zm0$n5bI#nk=eM`FUzJoUSJ}fB0UT5Zq$&etA%a0SS0YXS4_cfUJ*>7|zv%t3A(YqwW^_`@H%W%@W%{@e2Wp5Rmz zrgSEcBeW0KZHr79ql;Ana%;J28`U^e0bAAY=4yZM8*ij<@7R&{?b?+d+`T&;*s~`c z+`G3a6RwSW;9hTS-|p_M)R5g)^?2yb?(PqpVZ$(LP?FA_Q5}>aSk0qRqDkPF;MPsv zCii^9=cSM7Ub!D^G!#lW|6ht3D1CM*|%+*CiCw>W~<_yW;~gzx$m@o z94RwxI~*}6{KImAl1^u+>eI8eqvH;%A}o<(p%P%4Hf@@z z6f|)LYDWLSfU){MyFZ~Z+_`*u46xfFUj{o}TqmK^yvI%7XaG9l%WFb*8 zOu=zP5Lg$qHqu0&^+aWZjQ?M1)r3phEpaQJxHzq~wRLD}X)#9<1}vFDWJ_G$+&$Re zf7hH-PWdkMWI%Lc0R+I(rArg9HzyY6nR=gpb(?a4^o7!DmelqGVSKHl@zTemDY^URHg%1^u& zC@+z~8Yz7ZF%_gJDB4@heDOk3+7Q)sHGci{({oNgRV&~wi` z=bTkSUJ!TYj2WiAt!>J*YQTp&WBPQb+XI!BmOIWm=bY#M`#tw8fm&tA>i}P}lp{-8 z75w0WD^&h*A>JrE*lC+4Tu#r0B=K`d-OhKNefAyJgK#tUBdB155l5VH)@VRS-rw6B zq_Op4eEl=G+;WR2vV)HDji6S@yDCH!Z^9+86ham07&X!WAupDp>q6=Zc|lxA-KkDq z03Y&EGiRDMTLS^MOm2SgYhPQxZQZ&BScj1dDU4;3#EGaDDjd&(X2Qwq9>Poz=<7|$ z=a`6P?2qAUjS<#&Z`$;UN^9%AcCiyLYkEgV2zvFC@Wj& zovoG1y*3gFTOqmRl1s#+lc*dDQ$^>Mt^&+NqSZ`1LZ1nSLYz`COj@bdo_XSlITtKm z{GdJRDy&0gfZq0r4;h)~{WA>aoWh^Rzu^0Z0p# zVWK6#kq+gh=-#nw*9Xry_uOv3+j%Eot3~D5bez&M$BPAtR+>f#RJc|Lkia1C z{1Z6KjDOu zyyFCvEx96*2=y2|KGwzNHsGD0W|Vh%%{A8q{zwxkGi>LW@j%lF%!<8Ov$9YBOBihu zUBjt1e*bNIeo5YO1j-nm9jD7yD(Ayp`RudLrjlRGYJ@lJ63zfjRAU3vOSO@#<@KKiJofxopVlYj=KIi0JMCN~;w zmIO|ccoP*(Ubl>D$$K1J+pV3OHr)*JVuMJ765|UI9Bw&DZr;(|{Ru?W^>!T7?juGeCI*tx2eh}f zef8BL_gWw-h^(4%W!`T@h&7ze4QGe{>N0wdxL7oN{N4R2NMJi9@HiNs+faswCmwhQ z(}vzdCw#=#sLOTg(iQ0(2B3G|wA9Onhm!q6W*ptvCVpQ2VqqL$*~j0_uGe4xs(2jQ zefnRCi3i>V8jsp{xp2{Y-gEm8ue|bB)RvMQ@8{yfPdEX~G})EbjQUyJC_3E8nN9&v z9W0T}w)N{TMy@Khl}1~!AnrB5+9o$|+puAY2pwrSYrNK=Gue!hj*7A>WTVgw6#_GU zXd@9C(UT-vgB7R^&pq=@XUFvENQ-od*Clz22iAwDPoMtaowwZ52^A3Q5^DC4NL=(f zjdrOugupSi$*HHGev|lnOVy#cfIciij`J2Rx*0Vmwdjy3cqKRn z0HnkHv6)7pA)<~WFdT7XXLjViZrl2`YmpxLDwfgorIfzK2Ku0Ye;BZ#=F@$s8824lJkq+s83z#=| z?zhd5J1yi&0IHu3Sr$?YM;aoA51YW`>`OUV;S-=8S+$Q4ubKE+~uPRlCBMZ_+ zfqj`vHd})Zc8uu5fa>l5;C(3>bcc;bsSu+3h_6pOxQ#4Zy~%wv(4Ts?Y*`Q8x5!O` z_PzC%d1Ko)HwWHz`srrYQAe3(U_9@2xIhm;uJe?^!4;kJ=YNoPrINMAl2|0JqM7{^RYpN68ceh&2`>L&`qzo$r`O{_!8{IvyX*H}AXeus@haB1mClR&<_p(gznW zUYx92wJN0`2atBD-r|WT;TSVU9Igb;nw0cqVZAHc3_cvYdEL4tvu4eDx@gk(CsyL# zd;k5R3a=L+*F9hQlB@FT!Dv4c#+|p_Rv2llrSJ*weCJA|vemo?!gS?_Jjh(ij1glt z;?m*Kf5HP$c3Db_yqiPv`r{JoE_2B`Z3QyiP?azJ>%Sf~dF9-X-Ef2XiB&sB0zMKP zI~MW+_kQCW?gJh&k(8o3dkh{KV<&K*LXCF|>jWbYOI z{r2cS7;7K>$VYIT2H`dEERR3(h$#YC3is^U^WN+J{Lf#2ifFts2XKpK3TT-| z_=Lx#d_ww4KLG%{O{fKHNKO~M7t@LJ=6wzhigIF7eSRs zd@uLto7f7ZtL!^|(fFFy6E~&C3f!l2#-JPX{mt0G>O)5F1$ddHu-n$Nb?bU$oXxPh zZ%aXPeL)cIZFs^;xRDGd5J{5@0Qnz&;DL+_EGFUW)t&KdxDNv#35>cTAVlZVwC4j? zUU?i0IloKm!XR@6LQW*HxLx(}^k@`_r( zfZM7bYrmRjz`4fAy8uZYBQMUa$M6GLp~^7u%wy5ln*kXh2mP%*@%`^FF%iRE@+HJS zOl>1W{=`@k-bc-%M9F)O#8J>9@d@fqZGb{==SfFfo9oMLT7iNVF@p#?Z@mB!q4#PC zX+h?{d+4FueyAs-af!vAX{?4|GtlC@vYjz~`p5mI3{Ui6Gb*`DT5&XA0t-~4P6!$M z@L|diwr}6Q#QF&FC$GSIk^H_fj5hiO2?+yCW2x17L1Yc+yjr&&2`5F!UK9xo$UCjH zv|Rb*qmLrhQXBRe??Xfavno+H>LzfF1f5Lp-l_|G2dfjA$G`vmi;eHYw@67lF|v+D zvVGbbTUw%`n%Ay15D~|Y7DXa!3{EZdYQzPL7oS(Dgql*F7w zHIa`zAm{tK3DS4w%$aD_8pdrX3S!kb++?GLcoRlKJ;%fld!~8=C*uWCBidS9uR^UW z+d0ukAkB}AcgK#^B|I7xBIAVCxxQVI0av{14BNon;(UB4vN)_(WnqQe$64{ zRCoO5BX16CSFM8Q;XLOvEa12m|IT_HG~lxxiz*S*g#}}gzA+aXa7DqAdjJS0pH91A zy?y7#3pdFZ_RB%N^jT^_l}Ltr>aSaksJlK-hR3rAtuB<;!R?2S5BZ_=)g_%rAMHe3 zk@|Sx)Ww*A1gQt#EhHvfSoKP~dVIR|0QX;M)dtiC8h93{K60%hrCox=Xu*8+oH=l( zeb)?g!$El?!lwzv$;_HHE43u9K#h<_y#CP4Fu_T_ z)APD88ka@xSfAI)iRf(KrcQ1-hRHw~Vh09?OV^Z~0&K|1rACy(b$KD=Ce%?ywSF^GpZ8$A@@{6# zm~lRK6tR~{kax5p-LV3MYSz6pfhSrI7HYJ%x1ZuS;pGoXVwYqnc2RHd9z0@INbsp; z(2Ypo(5;{vDxli4i~G#$ze$Vr3A%q!KmwO&4&+^@gM+6UlNs+C0xmRipc*{n3j4X6 z)srhcB7ym$9R8CVd;K>PrGpOK+uNH=pFTacj=>=U7byRs#Q0!ZrE;_(gG^d#$ToJt zQitcdGxVCMSA}pQT_o|+rAtl3fOEhXX5--I<(FSh{gVy#CtDJD${^%=x3nxYc|A4| z+DC8QvL7%;x@gszY*jH*z0SQ1x{LWXaNz*&^K}XAVr7c^4Av*IlibNAfu{`QU8C`W zY=cb%fFsmaE~pW98HQfUU6gs$G~{X$I3~)uQJyiOb7M(f&xHY#N@cdYQAyybid=4k z_e3~oPgca%s+k*6Q-vU%M7oWtX2fOmVp216Z);Lh_-6fw z!$C6K#jbnzwk8g>awIb?IDzT_1I|8l#YW-!uyLZ3 zP2gI(>e_23T?jdb|F>*eA<7%D(YOxdb0DBbAi%}V?veaO9iE6Wr)%=6YDB%ht&~)W z)HfY+Lt;tp1Eo) zepl*M2#&}_I{;L<5m6FX^+M>KPK{N6e znsv>hHS7d1&N5&*v=J(VuZ=~02;<7BzVmeFI1Um4t|t7dst^)EHuN=cl!UsEXI&X+8Gqo>sgULXz0wN^|pc zU|=H_2K-l9VhTX|5V0C0Zx&C4F~dEVBKRc|xr>G(;H<}5P3~(-U{N9DA}R?v} zd7c3LzoI@sfw!uIlfZ+e3Nd96j5uSuO}3^Ewvvw!BT~oVh{rF-@pGwy-!7G#UT(ja2E}=+-HK3bYSk# zL|h!H+(zuo+HvaBc~cjtbqh)wV;PX zEeRZwip2}z2^^{fFMZ{~oFPXfcGvh97hMDi+=D+=O5w?mKt5~0{n7j0=Oi$>Q&ZpR zpCcyJPwfj%Ymc#=!-%;M?kfI@$OZ1ojy@#1jFE<%ATibv%#bh7l6RAzn-Y^7@{aAV zjFll0SaKplEv=0>fSm^JpbWd`R04;?f@`oTyNIJnX54F)A2LP$(KQo6e#*Aq%Q zrna^=CxPJy?Cci7}g|b+Iv@8D!HJaxZ#H1BdG^pR7v8=3%pxz#J%vbPkm}5 zf*6>L=LsaR8apE@V!h-|(mF;h8ub_%r29HR0Y^NW6WV0~5*Vt4{S&7D;K60s)?X5M z(n8*C|Di)`>^o1`eI}q58P1KyJ(;Rxp0u7Wrs^WFxY0N|Cw^d50pgLsj~H=sDoBiK zWbeLxj=-e^p1ctK*S_n&hx&kb&m@Ki0umUx<0ANEAM(qot5wwU72Tyn3?mU(z64RM zCAmEpjgy-|mGFJShHdy7pL^~(SfxEu&6Jp2fbQ${-=5RXJ$EJEIaYK^t<==s^N@-i z_2{FIx_%-21edM5SUN)YQX?kw7$3-xiwPvCAyt56*^3_-3cFX}-9ugTsZZ^;u9cr) z1A6f;F}Wb>gB4KA7A{D%n3oN*$X7mxzzH zIQ>_y<AL4l&cR^DX3bES0jo-;#ecdu?(@*Z2V;T^4zwfG$$JHX#!|M zwBW3>mbJFFiYkzZOO+6^8M}r@xk`L8d$6+_l@FX!Aj2IiH=vsbLk`u04;6;0LzH+Z z^_U;%>vJs=4|#=uqJgI9n#AaDw^=>uj0Jd>*odD+X~-$k*BWs@$9%mnJ8BQ7k?6K? zZsRCNC<5y0oze_9_i*wB_$8G1xuWU0l{Kdx9&-Rc(Bvv6yU>}v^>xJX_9#*-pba!`~KWy6czgt>c zFS0taX}8lv14IE~i!}OsEkOD{{>USy2(X5^={s-$tuR);=l$=$6z|uzbfZqzVn-#s!D2TfM$W;_p)$PUyKffLj_ z&|#_ws|RZiH^`GYg(kA%_G-H8(Kay}bw(K$#= zy|X|UPKu}tYe@G~6N#9YqzI59Wxe!0+YAXN0~JEl2h_U0r_zx7;0HO38D=cGYZ`8h zx(Q5bMAarA5o69RgTU6+;sP695)Y+WT3vWarbI2f{bIC@PwI_0=j29JgK6{T&E|zyUimZ*K$TQxsudsX_g(Yii?^99Y2Cza;8Y`D zM_%qL#$Z45fLI4EiEf0sHtQG6xc~OU3&N-SWq;zkZ{Q&N+h)7Hx@Q z1F8dncXr+9KKFYu-r)DO6H;drj=o9unYhV7USytm;_}#Vnasy}G3wznMU_AsIq^(% zW&2DEU!qUyF_Ga0Qt=mBDV+ZFsi*Eoiw%6;Wu{tjfsirYIa--7fBbQm5+LtU!pA;= zE2ICy|0^}MAIK%~ym1njvdu;{)T_L_U33zz!v#6#W#-pTW8*Iov-!Fb|QiyH@O|CaiJ ze`>0z%I}+wbbJhBiX5cb!4E|WC*%*-*XF#4u+O{Jpa+LzZMBrVzHZ&ekDfK_TUamk z;fVw5z5Dm?|4;9`LX@M~sjETVdJQZ3F zs1liT>#FmUoB_Dwwb$-x@96j}$U9^D^fF2}&fsX9)qC%@s>7GfkS?1G_4?4U1TQk9 zapH%@6AUg`aleixaVLhuVMS)_0_}~UFk!;|2lFvzI7wh&)T!_O1Kwn{&<8SO3fs_`H(D;{&`ys14%p{a-)&(S_IFbklA_ zM3kf*Lm^|#eZLQ`Wsd4HDxAx?zxvi@3^*({2w`=CkLbbvL6a&iHuN)O<|m$bV&#oD z-bjD-%rgtle%HH}SCZtElElr0xXXMW51U?j>7`5m<)xQyG@e{UTme)HRwr~=IqcKK zrOZaHN{k8(A#h|{=Gru-oEx1+a)xD8Pf{aXFFt*JeHEw>0III*z=w~Gw11VD>?{mNT4anOq`yoUWQbpdV zG>IkcgbOt2{T;46`5M`-Kius}ivk`b<1N!&szu`SU7Wyy{>$SZQ}??W15Ko^H{cHIB{ssrS~Z5f zbm`KB_vtY;;BLXPNMf;Qt)4Z=5jx{PjBBpHek1fUN&Jz5FEKV`ybb;RYoIoKh$OE- zx{9(>=pI3WQe=!LzFP&>b#vsD&P!r$*Rh~c5rUvPP=G`JNaic!z9XPFp$g*Ixj4lo zZbtR{N$z|EL(OVlBvbfUPkOj50sgB8&H-%dgPIe+5q^dn~{VaL+{ZGAAYx~ z>a2uyUsfT+g_iLpj}?uwgc7nQb3qF~&&0%)Z#d_jkSHCNy>2X-Yl-zxB=Asdd+oI^ zPVeaW3mk(M9c=~aD5c22YeH^=eDsI<`|m&Ylv6T^>5?JH+sM$C?V5Zk1uSa0%lI0H zjYdk~gqz%}eL!Fq{+J(PiFM(ev=&GcB#;+#OI~-q6HYtr?!9~W-X^&drCwYcTw|fk zdiL!3j3=)%)F5%u%!~Rqb5S;^3qoG;`2y6_WGXtZ2r}x)$sOZWhp=uMgoY&P5rr+Zw*%YN~~asS~~_zLV^fxfafWq!<_&aH@nE&Y$=%$e+M+=`S7v+8Mg8 zpS-b)eBNb0_`#R2zwNdj6AAZ1SP+3Z7{QX)EmOozN7`f?1XqPREy)nO5opHS&0Vuv zvX2}unMxewzz+vHs1--pFFLVogH&Gm)KeE7d+f1)hoA>GPD5+wDX1H3T^CY!`}XZO z{;SI`U+Gmax0%n0MA`zuIamSsfQ2-TPr7IbVxs~U!;h3U{5>>0Pm!%nN>t7qBY(1M zc+f;Q=R^=a?y|UV*bgZ!0>oztX?(__ML&Po#_xOk`j9>4dj5fKJXJguLR|LFpe&Hn zaMt3*GIpD`HN@Ls^ft`7XGz&6sR2+2yZqRJUhhp~c1W{qlZF$R+-TBvk9TutZXEPr zAF(s;P+;OG_Ct0#gF?6BFhkM0OzGTo(@j0^z4+q4{LOEFdk%&*K=p}UWbjgp(l2qN z!Nfh_TToc_;peMu8K5k#w`C&7N@Pv^yATTG>y>#Rd5sa~MGCX@4IDrmK;EP+; zGR57WCyO-WfnZA&5Tj0f#2BtXQu9O-`}TA>F9rFE-F;z@V1j;Zo$?!xU9jMzu(rj> z>+9u$2f-3&|OnRa<5me;OYb=e_HEmi z_U+o04(!>J4({Ds$O+HZ_vV}FTids%{OtHIudZH=uH^9_<-g9r#N~kUu_W$5Sy{EA zoyx9_%2vvE8p_@Zk``a~Eq=Kr^3IYP)8s%pJn~G8)J$Y9C@H)^U^7hKKw^0rnEVTp zDG=DsXZX3i+?h}Q_{Zn$?CySe_vXzTHJN)YQ4a0iJ>jIqGsH;Svt>)qw)N}p{_V5R zUI@aV=I?nHD?8axjxA#+n$p;^URHRg+Ih zH>5ha&@hiENRb0b0&4KK;nugbj4fPJZUfyWmL=5FMGo)YE;@S8xQpM z{&aUw&$1_$Z?DuUW84u-#()whkwoxPH%5- zAC&*48eh8HYWwVi)I+|Z#2mNIQrDQ!1%h~rGK7o;DkAT4+( zon>$kcC(2}!9#jUCYuC3d}^la6`nm7DjaQ$QZKA(5uO8*8L9^G>tFuzyxAQcv#rEB zueGIRK}%cPDOM4j4Pqyi%4~bWg4iF2G{t;yU|^HI?nJ&frr-gg^TeQx9*ceAIl<(gRgTX*vl%v6d2&V?j9c;0V2(1vWU>aal%XqMGSV_e zn>N_k5GSwFxMSR%pQQh$^2bREi@qg?r@p%?x57SR?ylbVLmVUvcX4FM*NLiBBveO7 z$KXQhCW!%Ozl5i~6o#%Fu7hn}8i#F=&^veT97MH_s@_-kZof!w%;kDY%7$x+1jgj= z69Nw=>$gqOe}+CE|L^qtKrrDy`Yyf_b-mz&_bOYWRR{R(^*I7a-l>*rB8d8kP2PW; z^An3i*1qD1gvt3i$?fK(v0u-)-n4`kDM*qtOzd~1X?pBfGb}}eG!==f_KSqg)DU=9 z^}OwBa{rNhx>75JGLiD4vJ3uQC52tt`*p50je1{G78x~y3L{5e@k9D4r%q^P3OiAS2IHYB*A^y7>qX5Gx^mD;cuvKbPp&~2@$ z-DC;tw%2$Do;%{o|H1zi8$kW{AZ?W@mXWj~G-CNHRQ@IEK&}wUxHBA&7gfU0%MjIq zRSJ{MtH?+yPGa!{i0R2yrL@TsmSYRmt4HHTM>a-&y5NPCUB8a_$+SjKPwF_ z^+Rf=o5ATW3GzP6&Fz<-fsmA9h8^&(A1`T0%E{Uu(jG63wE&Urm}bwOJ&4u|m{fj& zI|utQ_k*A$zFAZ_27SWGJ>*y#***6>vg{|f(+70)bEZ-*(q0YV%rcV=7-c|B;7eCS zgQ1OyL@&#vL(w!j9Wj!791Of_#-FGiH6)|<1_VDW>DJTKHCiLrX^jLMa(-NXe)&H} zg@8q^@?}?1l652%twf`sgn^FOxosx$5;TFiRe>P_aLWUeS1sH&CU*^DWvceroR^u% zbiIteZXzmz(s{LEra$1EPD{sx@;s7Mha(*^YdLJM7KV-(eu2nZB5Sg6=-T9U=?r-_ z@%XqZZmyhEt>y+Z`KY9pw7DRJGQ#x7t=*O-#t|7QT*HT%^H`XC+@Gu9YObe@WiKR) z`J9zYt0u`KUWDkqBl|M{i=}R?EJWf)eC{PC3iL7zNfg;uP1-PNlUNv)-Ek(Oa#T5{ zR`s3>W9nClf+6P;qL(F7C`RU(bHA#DR=!m>o-*4^ zL`5;x-Fz)Ebs+Lg8Dhpl@5WROM3lrWBD5+of=VGtDIsJn(oB&dr(eo?ME&4-H8myr zfmHLFaHLWBadX7v+Q>vcVpHOX0>7|ao^gcOa!#Tw_o_xym2HWsVqkHkP=636FYWm{ zF_plW_{W$_yx|YqOfpQFuQH`ZwJD7dX;f=*tk|GPrE9oFLP7_~IYyQ#FyKHz6hm$M z_+H%lQCj_SW64C#`I+^9if%LG)TF({_(Qkfim4SDX&jT-%D(p8KxexA&7 zOckn8la$PzVyH=sTB?SrqF3u{nc5$UG4xTy?#lrOIzFsVW&O5dN=O4!x56Vxdeve0 zKu;P}`%+b#Y@UW8Rl}%xn&txq;iK?t+P~tyYOmP4J N002ovPDHLkV1j3xU%CJQ literal 0 HcmV?d00001 diff --git a/images/fuw-shangjia.png b/images/fuw-shangjia.png new file mode 100644 index 0000000000000000000000000000000000000000..e3e039aebd09aaf50c50b20f6af9d5bb9efb9e8e GIT binary patch literal 12547 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 Zp8+w(@GRa%I%5C;002ovPDHLkV1oQ)P_F<0 literal 0 HcmV?d00001 diff --git a/images/gift-arrow.png b/images/gift-arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..f803d9d57006b7f7c2cae13f46ca52f645193e42 GIT binary patch literal 955 zcmV;s14R6ZP)128s1UQL&}RzW+d(1_NWt4w36FB)1AAN8;Qq6E2>eB}p=@AgRH}eBaC0q-oa-GI zfd3ZUR3k!A36%mf@iyh)0t)d5Ewz}99J@Z=ysqK;YfMe`FbdK8H!CaV1G7L=@P1J` zxhGKafLTx_M77oBEYdnb?fe@M-djn~ui}@1`W&5T!uPe00D`w%tvIC1|B!HqQWrPsM zr6|hR?(XhY@!tChx-+<$erIFc0s8IHJ_x3w^jPD1_c~Yk{r*|;26%1+&_kS#Hvmn> zYefLZ@o5L3wxY~f0iFd>&c;*%XOYGix*j0dROt;j=p~mz#)V@``Z%Ys1Uyt%VdahK4)HZg}n z)Fw8veP^Z&U>xkIgf2`r=CqnvUq(+7LLL)B^1w6+K+g`bs;sY?GC*)$W%N*I^Voq| zc$Fc~QzfKwW8uoqnC6Ent{5j$%Cbu40y9A++#+USL-oWva%(ROW>rVa2cjlmHbTiB zn<}9(F$;&<$3OkjHsk4!g=~e%z>HA6nS+@XEYjTbD?rOF7F<(dH19=VR_JoPgi2^g ztVlb49jym)8;zwiZ2HeEDnvD+R8l-Fe58|wk9LV`JwO#OI}G^eP>7$1mzIpfLb~~< zrdxR?J)!EKR*c3C*w7k~NKgq^83Cw-DnX*b07#+|zNtcVhhKw4WFW0%A=m%*VPRom dVPOHx$`4i&5(WJ01BL(q002ovPDHLkV1fmfvzh<^ literal 0 HcmV?d00001 diff --git a/images/gift-balloon.png b/images/gift-balloon.png new file mode 100644 index 0000000000000000000000000000000000000000..32875340f9165fc6364ebf07e7c4f6283911debf GIT binary patch literal 964 zcmV;#13UbQP)X;K@`TPs3@Y3qPCfd){26NSn*j9#0v`gph&$W zb0&?pv{WtVgJ@OoLao}}s?aCF2Ne;De}EvhUO^D4h$2)gB|Ax)-PB9Gl_R6o#@4`Y z&2F;NAAAsYL+1O=%$}JnA>;gKAlW=2#v&^v6Lkg^%}VOCx~A@tVkNNvrW*`S7)x_4ZCmlP8+Al4X6^yV@?N)+D6Kx8#324p~RgTbI}^TV_8O;QmmsCk2l?PD=N z90CT7pr|U4d0uWXcsEw_S$ZEY3(xn|JQ7kOskrxtkzfSlOByg)xew*MsI!@%(4~IU zvj8zFHe?%!FRVdxYcral|Bw|rhl%BljzYwPVx4CJ3?^>>9(<0#)RtlMzr|ZIzNpTA z2nx46F)st6;*ZKLZyarYNU3$l96r>%-1Ptk6}z$r?O$~`X8?2C8K(S7gf)j939h&vz@XxZ?HQ~)ra77eT8&6~{ofgd8e2BmJc&*d!5;ENn zz~EIb1BnGSKYM>Lf{CSja4>G+VE;QzRPMFAgkHEGfI^quHEr#_i0S%vCvEhO>j5N# zv$h}1xz#AktDSVA)2;`IvB-L7z1_azLB7_jzmEAhMvtuifSge~jd+1%qBZX`K%s?r zMJyPaV=(xdzv{!6zVav|ULq+R&07E_4im3I(lk*rc$9~={}`7=Cy_!RgP~cHif3c5 zJiJH-c9xMslAus38FY^!z|%O3&L+ixS^|>60ZGMY8&&^mFmdW-Mg3$_Y{Xb(uB76~ zYYM&HWjirjJrsHk22HAikz%KFR@IKqncI?gO1FPdQlHf|;x{a=9aJKI1PPXyT)%2^ zeLwN3dEEl7Q`VmM=6sW1^IDt9wXVKs`&8mLFm!2(#q~obXfYG?i~ZpXl=u-@dw!Y) z+5wa6hROBI7C-C$TQ@gIaczag^#K#K=gEi`AbuegZd+_|?YaeePYT)^uwE;^o%e63 zDqCFZus~0nTo)K^FU(2vUr2L(tqJ-ArF6K}yZ8RC1>xF5-v&W~B^GG6lvIl)eggfx mBP-Mh}^aR9ZTW)p-^2!;`?cnCF{AcT@-fDlTe zW)p873FqdGW5OVH)vsU-w?=!2zAw%rO~Zn3g!Euwv8n z(c>u}J;E@t#h3>G#F*Ji4x5;q;Fv79bP2J;BRn8clPJPPm`F0FJc_ z6HKGuD%EmKuSf(Syh;e*8e!D|R_7|fr=)}j)W4-__@5z@?sK~LwByx~t8;4rFfEDR zQXm*6(|>>naE%t$f{8H_tPVgDQo;kk*2Qy!eHDZ3?)hANJ$oqKGt|f1^e^S{Hhm%O zzj>+n?qdhvI!`@@xuRl?qExd^O`(Ji8L_ zES|hpt~1+vA^&j7ogS9~P%4MC3b6EM4wjP){#MJg43^%`IXyfo$pC1mkOJ@sZbY|d z2E$#sU|4tE=pD#;))H&ONMsvYud>0h``Ljocn!AQxb(LD%UZ&jfw6n*2FL`%vELht zUzLVH*ftbnGiw2Y;aFV(WIIy}K&2z}4**8j4InO1COFoayFEE-J$eZ;oxAhwu2>ra zk8A%+#xfH6cOwBZZ5au@=HLpzP&-f`sXJh(OI(}O5i-;f`p=J0J{s?m4Umtp;l1_d-46H{yvP(dPIs8EcR%R~aw?(DX^ZD;SDpLyTx z%+9iHcefo9i2IT+c{?-vz2EP9-}N5HWkzOX#{XwvH+L(O8&y9CE4LIDZpjU)AIv&d zu7RWtD*;?L)aQ*x1OF;S-Ud-P00;jAi88msDJ+01e-!&RUHx4i%rdZY5fYu1;2ARj z4q+8U{sKe+DatuG_&4Cr>z_=&Awy@Ky|8loVBti3K3E0b5)aMvC3Sl?upso+Lo2bV+hqjz431Kl_a@GJvbcL36^wxzXP z`KGZjp3IS<|0d`+au;CXYMEtdE(+tZe?Sm>dn=2(W&q8l?SpPlTFL;|OhpINRW%?4 zr!XmMFt!V|#u2b_y|I%U1Tp_Hh`b+{9tP@p`0c|GIx;&cSoTfun`eNQo!Ya=-)kVP z>jk(MlP=OJ@;)*UTg%4vHjDs~aE{F(pQUY|0JN0AzcVL}K=9i_1U?(537RVCdHA>a z@Rs#M;N$#60utMe5=EyH7j8IA*~WrDJGmDe!fDmtLr2X7(7Q1UKI?Frhfll&pM3;; zwtEoRmj`Jj2if%=0hF7{Az$8zNZm4e=da9#ui|b6dQqGU1rI!_5MX*;Pphuvf!ma2p!H2a%LWa+?Ai`#RX4~NOX1%$2El4`y9899 zXF<#e_}?E5|A!Bd3$dN_mERR3z_OF-N)CQS*TA#(Korg^)}Iw;{qWB(!25Q8n)`R( zkKn=S9TM={M3gB1emb3)Qa6k+mSkfdeglz-!G0M;In6w zFB7<3BPmU`M?k(@M!OX`zcRTCNxJ|~Q7jgmrrgdGpvElfKCFtW)<3ln(wgp&uWVAt zY#WTw@wr5R0aryO;Q#0$$~qJ`9R;zZtJqpbGQHAy0!mk@^f9V(w*}q}eaU4)-#tri zq*!+q5eR-c72(>~V_E3(Mo4RWAaZV{M!>gyn3g{MmAUY4z9XIh6Q@}X=8E)zS5F>_ znrVk|c&dam6O@v#Qy!@+qJWee-a+`p%j6p5Di=#3i`#%^8zL80Yn4}aY>n++?Lx?o zE%EyPDbWbV;$SvtZHA$`;1I)o+etE{c!s41{_o`CDR zAYeRj9VrXS>u&5CL|3pjsoz(J@?C6jzDclJFmkI|kpP zYv!E7@-Bhb+G+J9mMj~!RwDfS0>$Fd048_ZG*FvgUBr-(eP$TjPDgiPg|-Xq(2+_C z6}M6~mYwg?@4%;zAh36A$NzIL(jRCHyp=$1Pl)^}Eh9CrO+PI*+X0WQ1J^0hA1QwY z9_(Ql0U{ytBj~`bJJhF5qFwOobccGr2@QF}4Fw+~A&ie+eJIvS(VddJjY+k<503mt zuL<}#6539%Dm@)&Uf&6;mnlg>yJqB@t~q#(gu|3gS04#ii$xt*Y`R)3(&c#}=JVIe zZ8{EYp_tNL!sV`6X~r7oMrC%iw@J9%bwlKr8XmA|5pkQclr+nqP5pYEKSQ_DP2+Co zuBQQ)FcP9L9}d9+k*|S6I8LI>Evzx?(jh%FG9xoGBQrRI$A5EXY{qS_gcSe)002ov JPDHLkV1hU_^wpAxzR6+?Ql&}=nH*LB%xp`w%I>j}# zUNB!)wjo)*aVk~*6f7|DQ$@v{XgwiS{`4za02NPeL+cG8m+|4m)-B_+RnJVsWc(0X zFNjjf>P%$FpdiM@4llk1{ zwIJrc``okv6&siXH>xJRN9&=xwN5kLV|fF7#~QTn5gEUOiVchsw`$P5qY4^c*bGd@ zz+?iuUaWvUwUt@`OeRiLY~U0Rss@7}zm;*vMO$3c(nuzLR7{9Cq~|%s(@XRH=eAmf z%j!TT)12bUVz}#-oRTZK3U>B*y~3OYEt3i6!p{}l_>}$4+c?EX8O48Sh3Pgz)U|t+ ze;@)8mr3 zX-|WA#TE$y# zZZzl?K$uEZs&KMEPk=edtGd45s=_ngmb?Wp<84`7{LV;^B0a$<;X-)Qv(nMEKkI7+PStgQn33LKUo_s$DEU!L5ObR#-q!-()4ecp z$^=(VzYn3a&5-c5t!gykvp~qbPZ=|D%A{z-ds`rS)}(H+=R+>zT__)jIPvRZlFOfB z7(F2^>==A7e3J0xHZ z!(mhiTc2rgB)@FxI=o)EMWLIS}hB90x; zBx##Q(3aR<@7lZL_3qkEJY##;N$Qe#Q*8Pzb zTSQ*3U5WilPa4g<_x64E&8$b3q)3q>MT!(DQdeowdPQ}-lWJq@wCZ}3isMl<9Q$*Q zcQV)9dRcl>q_`WeDvqZsPWyY+_4ZZQgX5Uuw14wsoVKRbu1<`l_?+W?q`2*S$1S=q zjN87aG&esSIlw$y`Bib+fr?kV-GF&k{bl3;AEyi7#bpx~%?2#-7GxWn$1WOuF=o-K zLqfJ-DplMYIlxr9xHFY1099?cR9S_k>Sd5?pF_^|KwG^EZ2b!`T06es80WBf^g&;Y zp{fm-BbG6?M-C9nm=%2ZT)qNoV*_$-8?@D}ZsC`=!Mt(}?2WH{!!gcvLAHEfjG4<< ze1L>fx<;8;CrlKQOuUy!ix)-!U@r^}Aiwe@Wda_W&i^1fFB8LfE;N9+Y@QkxKr&yg z9|5>;go&SK)XGbt0ML{@;XgcU!Px?b^Hv=`DYH1Q5$;?GAAJe=Q&SlT!U3NV#24tnLL8ee$0m2|!v z525AS?F9vxmW#V% z+d}cIxf2|~+WxOYOmzNND2F&M+m+w|>CEGKXElBr z-})hd4?+Qm%jHvnJCHNqPUh<;ut>uAXLxlua+T2ew~_=;g#th|dm<=6GG9MISnXV| z%N%9m$9bdvq7gGQ=E*1jD8VE;(~xHTxijAGp!KY(K27#Kjc``&{_rWvKQ zH}Dox?A`GLsO6nO0g_5-^#2Dvu7px94Zej+_PFbpsJ1*wa^=pfUhSMIh|Z$j=zLmk zb}p{0ca}uUHyq;}uAvY5Vhq)mC&yiXq)3q>{m=CeHl!>wpj=oZ00000NkvXXu0mjf DsQ%yL literal 0 HcmV?d00001 diff --git a/images/gift-diamond.png b/images/gift-diamond.png new file mode 100644 index 0000000000000000000000000000000000000000..091e5b8798e88aa3cf409cd1c682b197566abb9c GIT binary patch literal 1067 zcmV+`1l0S9P)) zHEGf$ZPK(&m)Jq#4{(4JZPRp18oFem`(nFAfW!$@A#tD;qJw6fG;SKl^^&9!dD2Lc z{n_)q=QrPbF9gxFrZw&VGM#(GAO|ZKPgK+Np7=YRD+cX@>k!af#f2~kD;KZ#OuVg{ z1Lj~li8e6@<|GN3yXFoZkb^geGKs2?cMNP?At~m!-aQU=?PCx`7;!=o24fIbE*{X# zp7k}`qPYVk#lFOm3Z`+SEIm01fzcfvCy#eG6ACnKBrT%Ck~ zDnp3kpusZ<^{z2UMmA*Q0lvzGWgZGQHFl;jDO-5u7$IB#3chFZrxfD> z3KX^dKsyI0cKFDqVSZCYtdx+^pC{YDq8JbOukcZYN(Qg+OI{;|*X~p6b;ERv7cM)3q7Wu>2HLDpO&>VD7 z%oD{9kQAFfN%=Q=6VT{O@NL7unr3*wpM{)4@=ROB7Z+5-%M&RO(;5%ZitadMJ75mX zW-Xx%lv+mrOp%a(MsvI+l!2zeY*w}dg-uS&BIKIEH}%*WUjB)Skkl$f)zY1j#3D=~ z`nD6i+t#=A%j8ceJID^2xeF+6Un)nfeM`qQUQ~(l%^i9MNMK2t`vaoPeL{_q6#Gna zBh(JN#-PzVElJAIF^#bxBBuSEP=TJSB7f)v%?)WmOpc=@#SRi$8ES1y8#*PafeP5s zzMm5z&&BUKr_K>tT3eXCMAF>4*rxV@RmsYV?&0Fqzgj8pw@i^Qmx*c^?o27}yV&OX z_BhnLCqxGqJYu5WM>P8EuH)6t4WgP>Ntp_Ri+ylSw2deoT!5B}qgI)$`IZ^YxMFj! zwfAFn6-SLSpT3?Yoj=7l*t^J z&kyj+#ncKf^Z6f`ubtv3Ri<)e%Q;mO#Zjb8WzwZIt!k<`N|Z@>V9OrtS}TrRWfDqe zskGKbapWqKuxJU3TU`}LmNG@S^+Magw-@7xeEk(ijxw1#Q+tJR(A1gQKQ)STQZx9I z?!)0d@WD$onqBh_x9FU)1GLa@q7hB!jAeDM(G1b3*6NIPw7Nz;M5CInedLbb8NW?5 ls*%y`d9>=yqiIAH?H|Y6u?;1%EV%#x002ovPDHLkV1kBa_RjzS literal 0 HcmV?d00001 diff --git a/images/gift-heart.png b/images/gift-heart.png new file mode 100644 index 0000000000000000000000000000000000000000..90eeab2d8fc38bc8aa6b10820a5e4e0c2d388982 GIT binary patch literal 715 zcmV;+0yO=JP)zcuztW{68Tz7la_U73JJEu(Bb|0QYleNk~ zuA-ZPyGsb}2;&FM*jf{WUxe^s=;QGX`ye-8aw?}hP}87ouqn&7MuhU*Id zTLj^f5ImNPxTn+pQhBdt95(+&>VrXx=TtKhFN9ZxaX!kxf367BwTEXyA4;0xQ~dTg zCmvi*CEnz=CY+5ucqibe5t6jMFwB^=OFsJyFO!SUCK$_Dh;~<=kZ9o{_^^t+M@mGLh)Vgr~ z9WWZ-fQL8x$iR)*2AZC`PY4!mixk zsWg}xz*_n++*WC6-BnwK@mbFe5Ik=M!8VlzW)qnrqlYuEKMy+lRhEQ9)nmf&EqC#R z-%Y)&^1yu7wu_Y9W*+Aou(q_T*t$Km5b0txoykXkFV(81Nhgv+Nj!BheR(@oE*sD41a>XG(6Q1qFX&2I3l zu>7*a@d!EpCiVACUeqs?b_?OFO~k)medXAfEMEfS*xI8K$1=aP&5McP=nf%#+MRhA x`RKYk)xShbsV$oEo(c2Yww{85f&!HbzW_m?-wg^#R~Y~R002ovPDHLkV1o9wQyc&Q literal 0 HcmV?d00001 diff --git a/images/gift-hug.png b/images/gift-hug.png new file mode 100644 index 0000000000000000000000000000000000000000..ce37fa824f32d6e910416e6ee50e1a9f9ae9eda1 GIT binary patch literal 1207 zcmV;o1W5adP)OW{VmAY!BzF>jc zO2k!LbwSIn;P~wTiAGuN;Cu&?8L zO|!U1I@0+1-kI+^^UawvP+J|wdt8bM%k?O z{&1)Kh8+UOEeFWo*}eYjo-cH>!F_3mKtv0+0OXl*keLexa4Wxjjm*y{uzc!-Y#YGS zH%uQ4W=1M#g8)x!v&|TAZCG8ntNl+Reez=a4bG;7!iGgW9jXBtRCT+wpl(XL%|P_c z(xTc&y#S=Xjf*&7V+O=AL#FuSx^^LU6ZN(v2Gu_5zo~CW)2Mn#H)f0J5VlAC*qMm* z4!-6)6A{^$jJ`g*NiQAlnSe?{{AD%^PliLNM5B0eA%y2s9nJu4{Q0C$ep~OuJo;kI z_6>%-JusVvne*7b=*N02f=V=k&EXK9ja$p<*~B0=M^xbJu_(4j1Hjeu=DV^Y25fvGlYARGmDFt$46TB#Mn8A``^ZctC<9r#6A!h=pswBiM=uP@Sp~o}2DpIW0*j z+-rJ#Y>8|&;ls|DUj;f8MkN-NV5P^1SQwilLF|nBQJvDw%>1lL^h@730$vg*m-or5 zkJbTbv*~pHl~t&(8RbryC}*gz7!>7Y#|TIgIethUxLXIhHDfyq;np?1?kiyB*Bu3~ zyA#^d7I>4rgHKKBY9i{JAL$EtEeZWhBrWBnaXNzP)22d^?=$!YiDL1fy!vPbu$l*M zUIBjVu({EH^bWnN*F5W!Y&tJ&ankgfkqWij3&7%>>eOy6-kQD+g@5b#g54DnA@<8O zOS`5Kpt;lTPPfv9!mFOWWGEEggu=gq!b4OB%kzYE*$z=*X6umdnrp7P=9L@8^P&eq54(8MgCMxmu3kJ8FEa2p1i_2b zdu4c0yr`%Lhl~l6spwFcLw1l&(M@5xA&%9#y4dvV``Ol{O;Yq)nfEJkR$wp;VFr0s;cI1*ueO4`VE}5ecv4HyC3fBC=Ig&8;Qj5njn}Fvdb6 zAl5;6f&%}8EjfW$EOvk~c9w|FClZMvP1Ef43J8zzifb5Sy`1y?Z3)j9>*kyn48y48 z^LgmHF2b#s-}4BsxW+Jyidch)j<>u35$TTO{0U~;wim8it*$BI5#HZ8jsqgfwmST7 zA}ZDIY`I)UGMU_vgts0dDv32)RzQ~Jy`1wRngr*(DAsIC0o!r0khB|;?YQkPAcAy+ zBMoUG`RiT?3N&*9x2G}~{g}g(4<=6EPoQUr;pu!1=SDbA-etJ(Eal$`gl>Js^ToWU z@yHJhzteGQIDt84Av~JI;G3+!3-k;z^uNyFWwhY!`k9B^D;51-#jJ z(H2j@D6Qhbd!r#4rPcO-g|_@}p=Q8?0-I!8Pr$7KC8q|>a*?T%I43^yb%9D1h?}dp z`6`PmlNrn;Y)mYdaA(HE)fX8|#VpU`+X5NO#ppu83*A4h<1Mo>7AfNKeBLW1jv1@w z-vTA4hHnMelL#)3_hxh6?$7cv#up2)93K}DtW*KBT!XATNETg0vyS)ri?)Jd<=-~} z_fvsJ;6Z^+5{*W85s}rb2@sK`Y1&Sym2l2u_5ZLnif!AXfGo8d##kp2sp)juR8_Sh zrPFCstPzjL4@>PpmgOCaqI40_(MF^wN|#upE#U(K0s^FF@Dm`c+F9u{=)wR1002ov JPDHLkV1f;pX%7GZ literal 0 HcmV?d00001 diff --git a/images/gift-pot.png b/images/gift-pot.png new file mode 100644 index 0000000000000000000000000000000000000000..87e179b7ae070f69f659ab71eb16c89890455fab GIT binary patch literal 1025 zcmV+c1pfPpP)zd!<26LAy0r%Hl~R7jTD|tH z9UL6IDW&`x5wlXtzY&p`ySuxzzP?VSQi&|fBGWVp0NT&Ln&UOD;ac9ax3{M>T;6xJ zLsH5&x@r6^MEnU6A4@4|b#;{{CMIZne4OI(c*pCK8Hq%m-9kKmQQhF+pwgP0oTN&n zLfiup4-xT4L_FCKpyN1~5%C^BA=|bokx0DINVk|sBxrtqzV%lAYBrmfJO@C;ITl)3 zS?RfPy^Y+{4UzX8KuY-oi)?Le(eUu_shJ-c8BsmCx0LdG&jA(}7cVv%4Zb~Se}A85 zW@cz~bd+MTSl3O*VlkyXJ3Fg-anH@o&40?}@&(TUEX(?i&;HWV5^Zd3kmES&scdg= ztDA&B=43KSV`F19G&Dp50|WnRlxw(__au`^rNLUP$=ck5dvQhOp z0xd5ut6*zuYsv_-Uavn3z+;}PR;#LpYk5zpRC=kl%Zld!mSx>KMuVQxvaDPGj{pGh zl@Q`k2tiB$_;YS#!SwVr`N?cHi=G1rA#QfKgHM$J@QddF>2&&C0C?zQfCm8Z zj&>3lhVdl;Jo4emKLUU+v~EBM@sSW>>lDpzrqk&Uv~D7k$-E8#*8rf=L*uvkUvWGh ze@*Kpgb?owAw~e;Hvo8i0@H^8unYjhsZ{Dc%?}ucaXytweeAxSLB2HrAS;9@3L$=X vS>!o)jccy)9>Xxs`(ipkfB*pkXfB=t!i0qI?&!2k00000NkvXXu0mjf>1yL9 literal 0 HcmV?d00001 diff --git a/images/gift-rice.png b/images/gift-rice.png new file mode 100644 index 0000000000000000000000000000000000000000..e91855aef916da9dd6fcd8c776ac0b545e1626f2 GIT binary patch literal 851 zcmV-Z1FZasP)cie)EhytcY6^e2?~`E`X(y# zO)#)0?1?FgEmJWiqf)a6AGUOJcWze^om-_#bG4;TomQLZE0~fbx;3(;X5VdKn*x0c-cGkc~f*Qcz0M4fZ_oW1!_x38W*ry_Gsk%w?z@uEX)xEa4fDukmE85hpdd>({lz^`NJFVfDNrnB5j!?z<{X1NF9t zm|Ap^w{MxVuGe=s)G)bvF*IjE<=ZM@E+P3EU7xk2R6AaiGOIlY-IIFq=3{L!bAmK* zPG5lL@fJA!PEtC)I&o)Ei5&GQJQ{tByViRsHJ4*xc2JlGAiPLnZsHZXns1RZ zFs~(bmY!;usrwkvsf2A{WuY6+k*BCCJ`1a{8Vj>(xa=+HY`TexqCC(YRDq@mfNW@j^B>ha_HNdJd+teQApbYWX)1(1Ptt^D}yB zvLv2j>C|fsow~$YGJUKyiythRJ~F0Bc15xi0+q0zH6*69lr)1E42kK`CGFdhhZiME dlqd&tF5Fc0%f=}os1?vk0L6BB!?L`ohZ8uRO zh;5V1rW<#)Ev3||;1jjJ(=5h=f}&ou(rnfGO4Qba;!#>!^dMq|Ht*dwJ&0gNCYqAA z8?6evTi6eN7$&pf|8{0V0MOCV(b3ToLq>Z+w5aKr$XxCpIe1gTkr-cJqPXeEU;#7& zEt0IfvRJgWIASIHU&Xc^O(oB@qfyWO@j2^9__Jt9b9Z9L89ROksLd#IX_*N5yP}i- zExa&ez@m3T!9Y>K>ED9UwbRw%c6gQnne>S!|yDp&E<^w2fbfG~by#}a3 zn4tT{N*20NWaS=&r80996De^0TvO=Ip6^wqd|JQ&cjPKN)r1HarJ4W&U`Cjr{r_Gb z-UDhAUc}{v4fNZ;79i+1LrW^>0WC0)Z<`x3&^M>_)(vzjWT0<+GTP?=tr0fRB_RSn z3KQ^a?|KKSy0JYSJjQb&g6<3%;YOf!qO0KihV!;oQm`N#JjQciIze_+G-L$u7)&=v zT|fa$ZMLOmfjVyXTOODN>LKa3c+4FYTg$r2W@>&1NWZ0w6eww|vb?DFL>V zUBCpn15A*6<_d7V)lcBJot?W;xnQ|&5~xk(?bc$l-MW|bTV78RQd@xOViqz1_5%~- z(8SaP*f+J0YiDP#H+58OAqDD-b_AZWm2%}XaXsHtI`Nrufojx@0Q;0BY69$#{0?yM zxh^hm;xn4N#P9`YWCEOz33B~Rkc%@xwucF_E% zqWTU1P6mB^M4JpJG#H?nPSG!^X`>VyOyIP#0nX?~r|8$)4~$L%1z6VFyoo3cEJE9;*qOrnSh}?ti;?8&!v5qz%O~X8VkEuVOYft7 z`H`g?rC74?GLmTZ;)`hFm|rcq)=R4tOBS>nSc;}*gEFubP0eaIuoxYk2+F`>bX5Iw zN;h8ZyBJ%xvu{^ew$;sYJoDi7-ozfiT5_#?W@QsCSwq&cOrT+>b+F} O0000 + + + + diff --git a/images/icon-back-arrow.png b/images/icon-back-arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..1201d0f5867b318ce27d4000ab17557bd1f221a4 GIT binary patch literal 298 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?3oVGw3ym^DWND0s)y z#WAE}&f8gre1{BpTx}WIPfoFoo1HUPw)C0Hl-&j)FT6CF0m{#8%yG^(lJ z?Yn4($3zR?-BVKiT;IJuVYJ=ylY_)@+w{mCSB^jT*g5ay>WVn#3DT9K=dUsUP@AVa z{eu1@%hQ*xO*FkVzqNR8PM7{yE1u)bk<5>_HhhjcP^tT1k61w*x5awbBL{stAN;hr z{~*Xe@2L5$*sE)Q=rJX{R~6Y)f8^cw26K)>T!K$iS~`3r7-w!^GD>7kImC6MMReB= uf0py-*TmYClwZuc;JTT8?+^FgXN8MnnL=*Y@$Uutlfl!~&t;ucLK6V~eRjtH literal 0 HcmV?d00001 diff --git a/images/icon-back-arrow2.png b/images/icon-back-arrow2.png new file mode 100644 index 0000000..32f4401 --- /dev/null +++ b/images/icon-back-arrow2.png @@ -0,0 +1,5 @@ + + + + + diff --git a/images/icon-back-line.png b/images/icon-back-line.png new file mode 100644 index 0000000000000000000000000000000000000000..e95605a357445f853fa5640465a6d9771f46814d GIT binary patch literal 175 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?3oVGw3ym^DWND46K! z;uumf=k4WL9} z_2(+xc|6O{UO!p0sXMA?J@eJ=t~zZiex8Xw@LZnDaf P7sU5;^>bP0l+XkKW`8*W literal 0 HcmV?d00001 diff --git a/images/icon-back.png b/images/icon-back.png new file mode 100644 index 0000000000000000000000000000000000000000..ee30ccffdf36493a198fe96073ececa499edbbb3 GIT binary patch literal 262 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?3oVGw3ym^DWND7eGZ z#WAE}&f6P~Tug>MtQY1iEH8NI+O^g?^8>?Ni!*bg+8Fy6mKZAOU1`0b@`wHAe-qs^ zEgV3Nb>chQnB$j)P0UvC?pt0sy<2DI(|cMQW6qjfJftpnP4~~{t}`iSHqsdnynncU z75yb*_p#vg^Ihs|1@?*0snNIi8Rs*<=806?{@id*+NH z!7I21A*8@701p5t4G=nhD(DCTDv}p4DWC$a72s~|BzL!7pAGj()7+hA=G*;tXKz-i z68{b=WTdAdZ^_6GL4GF8$c_fTAw)N;)f-3EETP`&ENa6WBT4UJ+VJ}tdZ*Papx^Hw z(vX*FlaGN{6K{m(k_gT#heQXl=q4jNJ1WCRM zHX8E9JK+Nf@n>puuTCs+0EY>DCwSs;>ICGrtufvNwBftcPM-c4-}08t7^mO0;WcVR z7na)X1>XT$YPT1vO}?lh_cY{(8?Odl$>rECrZJlTT5WXCs6Zg_ISt-&Vt&fCur<2Z z+>x&}duLT((2zuOCae-NJKEhU|1K)SYidd$@O@_%9%Of7lqy758n|L~=*s58h{-OISUYATX*`k1dn8;=b$k&T;QElN-c+ACf?t`2GX}N@yws zLW4>IC}&`9BvjeAAC$8{MAIOUwI7tFKSZGkr@C4HJgYx!= zC^UgA{h&PkAqr2xFFc1mEo~B9y4$2w8oJmN{n140*1TjpkObf zI0F1UF5A4QXs~GTtI-o^fDwlbY^+_`6n_z3LhX-VbXD3Gq88+)00000NkvXXu0mjf DgdIzm literal 0 HcmV?d00001 diff --git a/images/icon-bell.png b/images/icon-bell.png new file mode 100644 index 0000000000000000000000000000000000000000..ba620b27049c2ee469c9db2bdbddeab433e0af6f GIT binary patch literal 913 zcmV;C18)3@P)Xo#$O8 zVR83)UM=uc9qN{K@g~)!=vW|0e?U~4(63wFQmk7hS%FBw7Dh3qwC}T9uDS zc-eR6JI^!o%*-?I3Q^jhHD2+oBXhH!^DNUc33((Z^ZyJY89T77U3eZ!UrWMeS zii)ObSNmYvZmQ)@#@{OepI2;nsUy$a6McI&$9V7$E5P>~HvV_{Bms|B)p+IkZA#-e zZj1!_s(?maP2EYz?eFOA-EN9`R)9U`-AbL3c5XU%JhL9c>UIG(E5Jgrwv)~s6t88J%M9+brJY*m__1iVHpYZrnnEQS*qEW zS&Y6AmXR7?vM@bhH~S23bCbd{QsZR60ncxG-wj=GQUEmx^qn;Vp83? zCjRZn*s?AWv(KB)C2aIZM3qp0BW&=NDn5HmMV_0-qhO&TCnVt8%{P|H7Gv9g;6NHX zrKZ`~8Lw$JiX5y)jT$Qez!nK~Qv!ZbKo5^We#h!N^K5=I?@S$2SHum)GD!t^Pr>P3 zrbvzVtCi7Obl`6#h7N!UA&ui}8;=iS8A67R;{!V-;FCWZuj#SXPlizd0Na$Ne)EUj nugeK8m}dz{Rxar^M7;P33yd+XN{<L)P!fW2x(kfn zp_{0z2T1b)QmZSY3(NEsvbsnDwVP9vX_QfB7{6FC!D?FjoW1s5`)J_@8{|0aoB7V3 zwbsm>DB8k*0AMF@3b+nD0A2zgfba5{zh~ut#-j&iw?iNX-*rxVc(h8?ZHv9)|Na|YEk+A1T^{xm~x~| zqHo^f_yeA6TOH~2$mf#M@Xq-x0<1lu(H$i?b@UB8S$0uQ0t|o&Mf0@msU)?HjrFug zWgr0u9rr|Mxdo;$vvE6(<4^(&fH6fi7L_T9byewg!{#L5grYK=yEaMs%w@R+0}3br z3yQAYWq3mw4W@M(Qh)(4tmpoQtWM)e^D z0CgF-b}O#;E~7PNR1;XhitMvpDY&*Pu5U>MY|ALXF6L}31DAZ>2nhukVjB$^1#~&m zGL;Y#rjXvm%~=EpXPpG8zZ@G&mHLIGn>gq(>M5jDVg0!z$Ea=}he8QRAf)7ZkzI2c znF*PV`Mf4(mXJ4A`BjI% z%UMVZGU9gOv6Y+P*GV@u*KME@3k0#24HP-Ig` QO#lD@07*qoM6N<$f+|s|od5s; literal 0 HcmV?d00001 diff --git a/images/icon-camera.png b/images/icon-camera.png new file mode 100644 index 0000000000000000000000000000000000000000..fe2953177ad53d1492e28e1e31c84b216499e74a GIT binary patch literal 588 zcmV-S0<-;zP)(YftX_-n1NIDJHe%#%8uIpm{@$oh zC?bjZSf~kBfGeQhjWVz-zdCRMtO1!gi}I8nEJVqnk!!<^Xp9=LA2{n}E2O;^EDyNP zG}x-;K(ZOlwJSa}@0{f(C@h->l9~^51dIeJ0}KNPnzjp5U`-1e4IBX&)3m*tf|^J$ zSpnDq9#jXB>t6jV8U|3ul+m% zZ=gdZwBTY1ZELBrQ1cOsxT?i&x!OjnYQg5zwO|nMMjKVRW$YZ7O^ZqWJgj~03G-$Oy3~RWT{suh zI1e9d+RjaZ>--&-Oa=ykTioZw_+d+#n6TWC(hxru=i}~@)R|#nr$J3=S&V%09*@Tp aq{R=`^oQ+W=MxA30000#Fz<*kQMt!$ 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 K@>Oz{VgCod2JI>U literal 0 HcmV?d00001 diff --git a/images/icon-chevron-down.png b/images/icon-chevron-down.png new file mode 100644 index 0000000000000000000000000000000000000000..58eb851e94fec29d588cf85ad3786e67eaa62f31 GIT binary patch literal 282 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?3oVGw3ym^DWND0s%x z#WAE}&f9Cge1{YSSQGlfZ{_j$?7kq|tbRQ>W0B+H?ptk1?GBT_tLp5MyqEo=A7nHT z2;M4Kzjpt=YY%#bt_jNT`^@|O(&wpa`?frhe}B~6H*d=9x5o@V8{5B8HLYIQ5W{K9 zIe-0u2WLwbuT}lx^P@nvZ}O>yire;=pS~YmwERlsT-(cc)*Sm3_*5luQA9b*k5?UuT1eRzG!3cH#&bM^kd)d)yZfB0CnOrY-N%oU%`sQ;Bq|M3^(Nd|@r XCc|{Ew+*`GA3!`$S3j3^P6{Y_POATo)V3I5kMrkNchdqVELpq kLP;A$leT~?{qmOA)vnOl9MB|(Kn<2eAn#7>yAk;_}Kbk>C@}WRywP={N#I>@tyN)j?N?}9;dky ztyYFz`(LyyURT|Jx5q-Nn8Q8iSd&$QvHZfFF&oM|gF7ep`ptgtIu(RkcYQkdZ)N$f oja)a_qi5D^(NI!S`nFqrRlC=>2_4@T0bR`C>FVdQ&MBb@027jDr2qf` literal 0 HcmV?d00001 diff --git a/images/icon-city.png b/images/icon-city.png new file mode 100644 index 0000000000000000000000000000000000000000..120b035615fde3e2d7dcb59d8af69038e8a7103b GIT binary patch literal 8180 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 agY*Agr*1`k9FOJz0000 + + + + + diff --git a/images/icon-clock.png b/images/icon-clock.png new file mode 100644 index 0000000000000000000000000000000000000000..94b7688f9330152d58c5fd78033d62004a0a208d GIT binary patch literal 942 zcmV;f15x~mP)L)P!fW2x(kfn zp_{0z2T1b)QmZSY3(NEsvbsnDwVP9vX_QfB7{6FC!D?FjoW1s5`)J_@8{|0aoB7V3 zwbsm>DB8k*0AMF@3b+nD0A2zgfba5{zh~ut#-j&iw?iNX-*rxVc(h8?ZHv9)|Na|YEk+A1T^{xm~x~| zqHo^f_yeA6TOH~2$mf#M@Xq-x0<1lu(H$i?b@UB8S$0uQ0t|o&Mf0@msU)?HjrFug zWgr0u9rr|Mxdo;$vvE6(<4^(&fH6fi7L_T9byewg!{#L5grYK=yEaMs%w@R+0}3br z3yQAYWq3mw4W@M(Qh)(4tmpoQtWM)e^D z0CgF-b}O#;E~7PNR1;XhitMvpDY&*Pu5U>MY|ALXF6L}31DAZ>2nhukVjB$^1#~&m zGL;Y#rjXvm%~=EpXPpG8zZ@G&mHLIGn>gq(>M5jDVg0!z$Ea=}he8QRAf)7ZkzI2c znF*PV`Mf4(mXJ4A`BjI% z%UMVZGU9gOv6Y+P*GV@u*KME@3k0#24HP-Ig` QO#lD@07*qoM6N<$f+|s|od5s; literal 0 HcmV?d00001 diff --git a/images/icon-close.png b/images/icon-close.png new file mode 100644 index 0000000000000000000000000000000000000000..2c6e02588a500c45114ad37a3ede5ec7c76c7c77 GIT binary patch literal 363 zcmV-x0hIoUP)jUp2M6zGzE z2?s9a_mQ%d8KWqQq9{rNfTJDwaCuxsaNPAR(*9lF(T=;m0i<(|;r6`f3RsnVwW~7L zeASQ7;;Q{efKLKi;g=IAf@?o$t^{<1kN2^i%o1cs{`YN#I>+ibwwCoL?5dS{I+!Kl zkk~9dhs5UL=SYYNKSe@Z_%RYUX`v7mSgN#h0fK+Q`2rIkrK9NspK307h%XcFIK{i*rdcs*aRCt{2SzBnFMHHS`)Y@vj*1Mv<2#Oa7D54lqll*7?O`5p# zZL5i-$)1_CDG_g26(wy^Fb{pOsGy)oky;hR`-3k&_$Df}C{?MK+7?qwTU)JGA6hw0 z#NCtK?*6-*fBTRRhK1!nXTCXe&YUx6jG4tOX3+?=WDss57ak(;dJEo6j`#jIEc_mF z{Dp;ojhuhXX2XrJayukOqfUTt1S3XV&c;m z@(($F_D+P;Qz0LL9f5b$O?>+JHFDfHliXV@c)T0|0{=&_px*7*;o`>y|sz^lc=wd!sg7?>M_S6b))D6x=_0i0C#0<7~-m$1+d3%z{ z*7=`HEaDYY4`Ohyg+Hu(bh8|vropfnIiKJ^FR_SwYf>2XjV>U^yOpx-T}$%knHfS) z&x*4NyhRyomvBj}jKEJRMTgSJpb>_znjRB8o=8delA#N=5&}PNQUXc2p^VqLc=)pF z6@XXkpq1=vQbOCnz+CJ2M|E7Us#<{sf31$pVUsc<_#wsN+o}XRvwXGQzSQcJ0(5r| zohI46kV_11HI-oDA66&N`=&`C@S8<$eT-PzK>r(L}e; zQHY$MDB6L$!#So&i26nsr~_qBMInO!sVD||`C`)~wD&GMSGmycib7M}gThnjcGDz8 z@NH@bzN{!@oqtIskCmygEdh&gU*h|LTn|;?ylt8Ua{i4XcXoV#MWHx1e4TQkgAKc% zCcvUa182y!wb&c-H&i3rqA%Oc$i@U-rrhV71j5p^Dq6r6lOi&iFi)4$7U9vV6~x*6 zZK@*u&7=tHc#Vp~zhewvRJ{WDwQBp`GAV%&uF<|v&OehxM~iN8#4f<3mAD*lRmR&c zB}LUrqy?zl=l3+OlbyZc0t>%Mkv}56*hGp5ex2IB4Q7TBgIfu_O_R6IKbN?TNY3vp zT0k!J%oOqV{{H!d@T3H(CQslGT0=OozS_g_e(4Ytek1NAF(H9X@CW34maCrlZ?*Gu zAb*y1JgV;=1@peA=dB7B{DQK=dih_J$xaaXb%}{mlZq7y{vA25ZOYtvmYW<#>OY2^%)Z`H3i(?=rPQtTxjm z%k10%>v*j0dP|ql#~hP+Vr_(Q%T&qxRSo1f$pc}M*f}Q32Gsvsyi*?IBu_}P)ACU^ ze=WJd1plI3iU|IDX-OV^YL#F3PsB(Bey|fmS2l!nAxb~o@h$>CFQ-$(->n)A%5rAX o%6>vDB{3m^-E3a7FvmIm1;UDC3BAdILG!M z3lWJ#LM2b~OOO~q;%CtY>Nj842L}inkQkk7gbiQU2L}`X81I1k&DZt8f$^tXG+);T z2ckgLM1g9E0@V-&sv!zgL!9PohzrFU;#MOPi9{liPBR*fE^wcpGVyy5zUx-c!8O7u zB=$l00s*ZiPraG0y^>5vhD65l$vvOwOu6 z3hwhAZB0tCBIaEng|=`LNKC>U2=rVd&?l#X+ooxr+v)aAO`-nkG;rHA%`;E(rBZ<; z|8^RUa?GtIppod)juG|$AZod#~3CV3Y7 zveNq?aobs7A2iu&F!49d40b%%TaCPCvkHk9P6GR6(ma{?zHa0c?QpK_jy%bivVL}} z*izIQW*|JY5?BOARmUf7b7{H4xoJP+0d=Y4-PZ0>lmW2t*Swl&1^)XHag0W>`LNPQ) zCGYX72n1ux!Z0{#g71hxFt#j=f~Q!~6~L|j)&u7W)a7TxvM@naeEvTzmL+hYcE-+= zaBW$b!oizD2NZS=eNxjs+85LgJ5aLC(Ea5~+7_&Z5rp z)fDcwnz?1+cy{Zl6Irc~8k?3yON+^6HO$qyn!;WvbpaYR*Oo;j5{X12VwU*_3@2ti TRf$2K00000NkvXXu0mjfrS+AY literal 0 HcmV?d00001 diff --git a/images/icon-cooperation.png b/images/icon-cooperation.png new file mode 100644 index 0000000..62f5ab3 --- /dev/null +++ b/images/icon-cooperation.png @@ -0,0 +1 @@ +PNG placeholder - 需要用设计工具导出实际PNG图标 \ No newline at end of file diff --git a/images/icon-coupon.png b/images/icon-coupon.png new file mode 100644 index 0000000000000000000000000000000000000000..7465f28e0aaba53f43c92837554f580144fa7752 GIT binary patch literal 457 zcmV;)0XF`LP)K`K@f)*Y>YmG`Un3O8=_U{j>B7?gu@ew=i>Xx9p^RFbz{da6R7_6dl0+1nIHczyPfcTFx_Y zSph6i%5DP#ppL3}290-<^zr;DEAER7fI6yeu|N-@0-&aQc2J{$;7q6`gy76b@}B|d zq=9%kCPYhCQ-VNmld_dScF~lu z+t{}fWp3uMh^>SKfWR|QW}yUtDVTv)&Lic~R-9fPTxZ37@FS&Zn2H70oxHD*>vq;U zv&sj)!r-fnNx4=*pc|{B6)$-~5ClOGgnz^b2oeK=aV@Zz00000NkvXXu0mjfnb^q$ literal 0 HcmV?d00001 diff --git a/images/icon-edit.png b/images/icon-edit.png new file mode 100644 index 0000000000000000000000000000000000000000..d6ac08e6f9da3a3c5250a888823324721638a979 GIT binary patch literal 694 zcmV;n0!jUeP)GV7BTHJU9TzVm3=oB)6Wuo9mJ zleOR?ZhF*U6Qf@i0Scff3@)^?uQo-SK; z@Dl;?_gvl2D2nzeCmI%K8-T~jQLE<5u09uLP8+O`kVf{bnz!vwn{48kVw8|?RMn5C zQ^COP*5D_*?c?xrm9e~uYV-UFAUX8k;UVNJRe*yazCKyw*O-`6|HBXCbFJ>Yz1FWV zAydYVShglG3r_5OBg53ZzE$|5iBhw61AbCpo;I9z1|IBsGU)*MUN_;fL$QG#{twje zJocI2x%${(ADf-4j}5jJ8ysG2dY=E={{#PA^11?u*Eg5~&3XW>Oayl5S3pQx_D zi-7pG^%Ci(a`{x5*V5AP2#^`&LHxj+R9%1<>Cntu|4HAeuE2```GIBNzUm6R2$0tb zK%tpeIRPbe(i76yPBd%@uDJUn`KXq~O74th&MA12>}=^xnAOnE!i%z4%z`alye`KM z<$^SsjRw%8@pFO~<%zxPUrFjzI{$1(WKDWNdf{Od9agStKzu>Imdl+OPZ{ZnWUg(l cUdnO(0;DmCs5YYnl>h($07*qoM6N<$f@x4w+5i9m literal 0 HcmV?d00001 diff --git a/images/icon-eldercare.png b/images/icon-eldercare.png new file mode 100644 index 0000000..62f5ab3 --- /dev/null +++ b/images/icon-eldercare.png @@ -0,0 +1 @@ +PNG placeholder - 需要用设计工具导出实际PNG图标 \ No newline at end of file diff --git a/images/icon-elderly.png b/images/icon-elderly.png new file mode 100644 index 0000000..620054e --- /dev/null +++ b/images/icon-elderly.png @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/images/icon-emoji-face.png b/images/icon-emoji-face.png new file mode 100644 index 0000000000000000000000000000000000000000..0aa6f742be569cdb1dce6e0af06370a4567aca2c GIT binary patch literal 1979 zcmV;s2SoUZP)Z<1o( zO@6n&=~b?mrEs@Y_AcjI(+(MI0~LZ)^tklxJ`d>+plKO2y$8yE2-;qX1kmzm$d z*B=F>XSkn>R_3PbTdykPqEfw0pqUpwJqw!N%1t$zg_)&hermCqt1L9L<$Clm^YIgx zaXq&yOfNNal}1x)dJ`}_Abd0L`R3N4GM+~Ky2LB zc%$i+S;y&ZiF8OR+AIBFQTo=C%G{l)%zr1Bo4a~I?P8V5$QFFWSt=p@SI40fwn4j zgwS-}RmP;#>91VTKpyM7wx{#c-IWJLpGZxIT%?pUL~h~_&JCnhi zgdaS`Rv@+ryq!{+!x`)_KWy665klD`kWk>ft8DN2(h}Ozkk#gL6wL=np{Hr3 zy{M~&SvIfsH+;YJv<6KN0@8Wismy-nD_Y>G z2Jga2QLc$rB?eE#{zz-}>20@clruskG0;-esjj=;XbRsv>g+59+od1wi+!EQZP0XR z4DC0a5u)_;^j5y$+HOtD1xI??kt*EHkyKm}9bn%+<%}Q+-~P07C$Niw_9z!V#|JhF zHp&Z4spt$SYB(cEQkx~I=`8cf{DP)`IIRGj-t88KCuFhaIU`8!WRT;-8!tBdYYr$I z1>mhHiDT68&!SY{m}qE&$(>wGfu`eZg_vIsL;0q=2uL4y^h(=fZmxQ#GlJy$)jK&- zi_a@)y5vZtPe9YBT>wE|7rT>F7p!%#3h9{&yqE<)+ zIj`ReUHA8Z?7xY5YZUEuu7{gg+ar#gvUdT7&m?^zP;_{Fa&n+N&#-Bt)cAJN0=3eS z;m@K2=P}IM;y0r1=48`dd$426H#Vvf5ZiF?+Jp zfZ@Lcn(P$~l1imEb3UH)@=0)mEfeSOU1h?dFt1DlH2rlAK7e%2)j@E@_y7|lB8-eM zLJ|wXH+3Y4rWH1A@V4o>_=#%rhfz4T{s9r(-mU(7RIO;t6GbZ zMG?^(LfHd+&F_%7ZIH3Ky--1!`|~)-aruc5OI91-)CPi=`iW6OMhhj{7a5$6?Qx9p zayCyQo#OMs-l8m`H+eIjhrc)iXGjUNxC?(oI3an=b zQk(xp*>bR5DDxA*ew;Y$$BEB^qLYAh+|%Jpj1xz1O~>2iXM1tta1Bl@k@mRdE3h_$ z(!|8v*Pt9Kz6n9w3+p2F?O-F3jD; + + + + + diff --git a/images/icon-emoji.png b/images/icon-emoji.png new file mode 100644 index 0000000000000000000000000000000000000000..16e7e92a04592dfff95ae2d2169650353b5a99d1 GIT binary patch literal 1246 zcmV<41R?v0P)X9&iG22n6^G1UL{yB7ws~U?C9!c9WoxpwBbFOt)uj&v+)g zC?&OI+2wLocXjnwpOv~O{{wl5p1D%TqBZfWhU7zI<*>GV!dU(j1J|1P5T12(T=dA| z;uRO#??%_@_l%WOhU9#u(RpVdf`P56dfv2-4DnZ$ogO^C)QQBCcYMBmlOg`JM#s;4 z81H%`R)$nsN$`1ipn!QW`&U50pT6mP$ChSIC=a_$lgT;m>|8R8{Pa%7MlP4XM` z>h!%~h<6V7*jYooyG8E)$~I$pKwH_nzEaz9|@BfY>kkNbgH+I|aC zCx1nFX{;c+WznCRRYRPrnx#qXH1S%_D!(*F&IhypV=Hl+)r3UH?ruY};(xbk<%vCoH#`u*`N+sy_(JC!+;-Lun&^sYKdLa^~|^B1|X#Sd~Adg3VNW&KLSY?1EGx ze!NV{^iS9W>H>)--rv6m`HP`d3E0N3^L3QUQN=8zw}t0KhMn>y17`iR|4z|Ibj&Sn z<$FW?gHGIB?fS%fz5Td%ASCutWtkIJ6EF8xs@nCFbc+|jTC3$QFH9d`n=+^1qytcZ z?IcHgeT!l@6B@SiniuJ=62>Q-wpSH*Dqpfrs$Z#AL^zH_9xBkp9|THh1HAz5 zQ0qimiQDW2a41mH#J}_^0mG|xBCW0NTJJpgIZ)ChXL^;;h^TcUQS{tpUUr-=41kC( zRU0V)Lvp-VX>RV>Yt%+cA>(ag^sr^hAx4wr(W*}29o>B&w#MGXRm-S%Ae}aGI$zd# z=w|QGzYZ-A)u(i^u#AT7W7dzB3?N(9zck4U!}boIK?k~?L(7#8GApm-MCl?Ai5?jS zB@A(5NX`YlgXM3vmFJsD`*zTW7`VngP4ZpXYsfj+8sdKgHVdId9+(65QP}*9LM#3^ zNoSBA!@d!bd(d00)m$(K(N^B|(rEWE<5>`K`miQGR^53%hIfnXclguZRFyqLl!G)n zTc(k3KDpy6P4Y&Eaw&9tRa<@?@2CQasJ+Q=XqpMn4tu9zNIoC6rHp9Xb(;8TREexC zTlu3q4&xr4L8q)eL-Lt7p$7{gK3HOZ*3e+9DN96dN3eP&;kWXsK6F8#I@FY;N$I_Q zaX^D#+5jl-4oloO11r-Ywp#P<){vd!Uz_vWuCHXMLxl&Ma3xCQo=G`xVs~FYwdiOh zgP^#>gGx>rEA5HoybaG3P5k0Gc0qJ4<(5OspMbX+-|_Y)zuNRn)|=zn&w|KZiGPSM z{G>?IEfsnEM)!o>=i{QfWykX!P_wXzOW*)XN{aEVc z$T?pmpt!*jkBWUtoYMO%SDR4;EHQ}I`W%;oCg#V~l%)ae1g(BVw>sy z==KHh78o6kh9-GmTlpG2HsErXEI5vPcm^Hl4ur%PMQtqk8+X8Jtv>iaQ2+n{07*qo IM6N<$f_zR{(EtDd literal 0 HcmV?d00001 diff --git a/images/icon-empty.png b/images/icon-empty.png new file mode 100644 index 0000000000000000000000000000000000000000..11dc43b0cde67dbfd9e424dbc79c92e608701027 GIT binary patch literal 889 zcmV-<1BU#GP)WXMNlDVP!u`b&UxmHXKV}z=ztmu7X2@d6h+uM$9GPI?kAmO`R3+kZ+B+r zzs{AEl$0zcjvEJK)-#^%k{7#=i&&?Kb=<#yf55Fpk|fzqVz)hOOk%-{nrD0C#h!Sv z$M{XG+PO!t#WQZTdHebEM$dMgM1OkLs%JaT+}hjCeoj8@^U|0kws&|YX1&&Kw>P-e zfJk?zL5sD#*vSyWx~$K_mbh`6*glZxY}9D%a>IlYJ4$TdNbGsNUf;A*Kh;)i%(G4t z+gC4o#0~4qBY4mDpc2C9p#CZ$j1t>J45ILvm|yc+#g`8ujO69F62fRY6QZ+)<^&QJ zYko#`bKbtIt=1T^wMq1X%adA74|&!^i{X}2%f}#N1_wU`GO5>F$mY~eQdAM47$7^(R*mjdfDKM zd9hzUv9p8N05A5GMDM!1fHOE<&aPL0S%U|}`ixlbxV-RemocrGZ2u8xKf!}e`#t6I zA{8T|meqc7!2Jdf7CKy)7g4>wzu(ffkgQpgdysc!0!j*krRV`rq8G6nyS(wNx3abp zD_ueXt%O+QS#P*vnmPN9O|7UTjzlNWFf#KtR|L?okb!7@^Dd)N)ZRj@AF*k=qJY?d z=z#cGs>TM{$SP1}qA!VUMpf6lB7y9K{DTZ!7{nm0L~O0=Cnv6=a->R1 z8k6Yfdu>t>ENQ8nm#0?>U-MZ&*@Fy3VDra^)?9S7_@wi@iF*rm9%b3BMbcIvX`V_C zbVcu_7wZ)M>oq$=0f~o-QREf(P-^7>O4b^24mlAQx#s`-0hE-Kl$87hIn{rPQsKo- P00000NkvXXu0mjfvbn21 literal 0 HcmV?d00001 diff --git a/images/icon-errand.png b/images/icon-errand.png new file mode 100644 index 0000000..f55a614 --- /dev/null +++ b/images/icon-errand.png @@ -0,0 +1,7 @@ + + + + + + + diff --git a/images/icon-eye.png b/images/icon-eye.png new file mode 100644 index 0000000000000000000000000000000000000000..0f42b6ec33679abe4e3ea0fb93d7c359ff3a75d5 GIT binary patch literal 980 zcmV;_11tQAP)y7}S`EqN!O>ws#&o%-;NRc8%ikRrk=bMF1LIB+mKob&ZUI4zAioS7kzui8XaL3prSjO0H zm_VHp=z#!QkwCwqG?o#7r2QMtu+F*@?-?i@mI@yyN1XIsdBisA zLf*_3yM@A~00IjF@V*63UIHHL9w;7QKj+5Z+{gcT43G7lgu*4Zl_+@$&UFd2=3(ro zyCXU0gkcz&2r4ryBlu9Wj_Qf5S%BA);GU6ycfGe8Us-_daSd32?E-Mh$EEH>T$oBo zz_BXD3cm|QBc)QQ$w&yLQmKhooOOE~@Ubl^6+l-!T;`)?7f%MsS-C=6dEz)H6@76V z&@A`yzaUv@Q-=gv^w{mok;t32b4miPR+6U=CJeRs$pF9(KA=86z2Oo+cnpvAwGBfp zY)jjPRpwM1`F=biftED|pRzW#29cLQlMzDZWY8qCHny@&O=g+7g-&2S6{v_b!p5#( z88Qro`V6d?$R$90rWg9jOmez$@li=_f<{w{KfqqleiV6n{5k)Pc?Ub*VX0h zu3kwFW0gd{n&+IOAqIE?a}{zqr_XP!1=z2x=xmsN=D=Jw%XwaMRoAWG}MDr4sn8ZmC3^ zWR7>DYSlEi;gt^~FXAL~antEC0UQ;IUQ58w-1wXO!hBxZ)Zz7Pjhs%0J!m+L{qql+ zb!2$Uz1;z)>i(oucstgUQoH0lAN6k3+QOrr3tFN9kNQ90P#UGIBH_}(jj{@Vn|9%N zEoBu6v=|n)w+t>iaq%f?%#Uz4Jk0hxhm3}l78On>Mqc5xm7~)}qfstS;dq?lt}LIQ z?S*L$@7!Eocb}A6dA;2Cx4h0WMv4?EQlv;>#KUj@907Ef?}@np0000EB& zGhFXlRmh3x%?cB*sN-8%zhmML70JIxD>nRp+TlgA)1%HyY47qDUrn0E zxZqyryo!Ck?ponB(-?U_Iv6oMYG1$cGY0wf9u;-TeM3(0yA&O+c z_YPPJ36vBRJiY)LBuYr2LV|(@Xn{~*kf@=7@Ct=Z3TOhF*g@IxuD#B#vzC}6ZDY+n zbI!eYX1z0+jDv$iWDMlb?G~WxLeU$kcu^{imGCS3yF)D;H8!mub2S8_eS+0 zn43~tSGTfGltFaA8@LOx*NqY={ zjS_S_pc0n^^yL6^w@gf?DOo&i%~eUj0R3D|YO)nFbb|mrv;@?acPsyI56svgg4}qy z&yG*rfSDw0Y(v1e7Hi|nG3ae!@mZ91ByM1N&-V$@Q?15((B&`#IgoA@E5R47nAh*RX z0lK$(TaF_BNBY9q-nkYcnW$$zeZew#6PS%)pcmSP!>aH(N(FpDk@~Kec%(B>hpWKa zTd1cO@yE6X%u6F}p)&lr+tz@2S(ukb4`zQMhqL?I8u({I@vQl>N?o`z8fYf6cpElh zuf~PK8Gx%mJh&EuYoT~>Ed@!(nrDYuZYj@FNlmG*Q?$ceEtdy_v}1ikc$QS@jq zultiWFqqf<67<+V7r%&Ypnh(j)?aB&u?;jXC@SkOo}Ppq!*>kxF(JXv*3SSvjnbx_ x!dzgmxjM4%3=+vWxMsK;ZIevq9UT4x`~{lau|v6H@nir1002ovPDHLkV1kaETnGRF literal 0 HcmV?d00001 diff --git a/images/icon-grass.png b/images/icon-grass.png new file mode 100644 index 0000000000000000000000000000000000000000..d56437948472904c80f4ad660d6b16ef649853f1 GIT binary patch literal 928 zcmV;R17G}!P)_H1nIgW7mw` zJDE@2r+eR@`L`~q~+ED(C@ zZ50w=+6PRBECFrG7sOSWX6(+MVeAHJoRsL*JLrbzRJ2DR)Vh7lK&}mN zxY{0Ah$)yJQx~fn35;xsyph+SLmlCnAau3gs*x;%kG0?#EzzaM=U(^GLVK)hZ)F6u z)Q9g+EY2>`(d11@*Tn(cy1wb=sjaz?C6TN5YZstlaSGOy$Y#w&RZJ|TeV z!(arYR>hP;Ya*Ux#^HWoJf2)V@+zr41@V5OTV~8Ke$|Zke^rX7AoMMVkdrA(X`#=9 zn1I*Dx~dQao#+zl=3gq>!&B1}+jw6yUh>)&0MpKD-Z)P4KvxhiRK*%5tAb%X%G6^& z#48jyyjU~7%33j@W4ZGNznYcy%AJB>5-zLqIJCkuwzF}Dy;T6gk7?JMRe7TYZ)(P) zTB55$cf#YvK z@2AW@RKrD3qFX!@)mVnnp^6F3c);o(`&BWa8FyO(npH6Y4mDZL?1(BRK zI5htQl0P5{J$M?r{{h7>Kn7wV^sj|VlqgX`6_THMc!Btj3K$On00009QPE3Iqg`)CNyw`tY7Qn- z^%9f>6jAgtH_;y;Q9MTw^`g;>%TdimMLme|BzkfI;~FnY#T=&J^w{%ynSszBG&Iv) zRbMTys%FOXa^%R77>Kx7_3&;$I-@jQSI93x(`TjWHz3c-J!pO*&p`9(5}I8iq@-V5uR0=$Gf_Yu4)c>O@|cn zr=-L~GV-ygxj!@F+l*PNkS^CHq3Mf4ngDIc@8#{1B}7~%a&lk9@(w^^(0t|JyLBD8 zC^D&%FO|#+g>!DPXN-7 z5dD*>WFo9=Byz>xp)FoY@O#Qz74mdQCdLyf1~dmlQv5_}+DjRQ?vb^C{IoV$<-S-7 zg{0kWB=T91uXaLddK0jz=|ij*D}COs`83PszvZO)r)B$s^MtjBlpI*ENdV0!gQY~j zia6ZSumV{R>{gki+r;cn)0Puw8xoc!%ie83mj}Hj1GFKlBfYIuDvKfoWmB{#Y-&Tr z0fm8ql?v%=H5ZxYk%Dt8aWHigO<^1Y(0n!M0G)~!LcS3!A46V?P((7;S-ci0Bwt3H z)0PobkJ)Ke!}CZXS!=6>N(anpuSF=xV6%o=q>$bJWhM4n1m7QRx935mkiCmEg=w06 z7MugBcE$Y$NRJhoF-dU>9NU!UVL+Y|-BI?FI}kin&zyD?3;X+rHiD)HE;dtjVE(!< zQFJ9QYXLZt&&mOrkjB*kKpKf-%h>_@`M&=0I)!}8Iv0=J9pI@zIU-GpZdX3e4hZ&k zn+-wZJmczsy{IWXvOX@KSOQ7gZDw{(!gK(#$NPAXX7ucUQ8mU){u5r|Lba`-kv{wKnY%^S@fSEe6Nfj`pqMZOY4OD z?JJ)Je8wRJi&mXb!bh%QHNF!M65#z)X=gqs11%w}#2OIB7p@^Sd6X8S;itxryM|NC zi2P0{!)@1KY90Q~-VTIcLFZs-IY#(AsspcGgQ;y0)}uPGIXoSJ&4^cM=U_I(1{l|d z$iQmU9(X!51{j`1j1b&$4W_n1c&o=igYb|LHj6(#*wl;FgFObi8~IuB4Z!0__`m+X zihGzNYzFL~h&9mN%<~1V{(Y0A)oOL061;K^t2={m<;}#zUSp$;NjxHip9)iXhL7P( z#_UE7!gI>-)it~lehbLIuq*gKCcJ53+;UfW+){>`=|@_FtHQADU~IO-wM`Zh!Yg^2 z5Q6UkdDk+5&)b9y0zNMjGD!GAn~*%<2W3L?gwNW9xPZ^fgt&z7*F+)U)0!w0{4Xhc zGd3oE-s8=fB1#iX6dFFs#8AM;?TJFpe3Al!OF|fQg8j>h50olZ%B0Z`d*{&y8?a}> P00000NkvXXu0mjfe_T6_ literal 0 HcmV?d00001 diff --git a/images/icon-heart-listen.png b/images/icon-heart-listen.png new file mode 100644 index 0000000..62f5ab3 --- /dev/null +++ b/images/icon-heart-listen.png @@ -0,0 +1 @@ +PNG placeholder - 需要用设计工具导出实际PNG图标 \ No newline at end of file diff --git a/images/icon-heart-new.png b/images/icon-heart-new.png new file mode 100644 index 0000000000000000000000000000000000000000..37e2c11848cd68ce9d3967b9b16a82c5125d5257 GIT binary patch literal 23970 zcmV)HK)t_-P)~8sydc`y zy7MT*%sR*b3JQxC&`wYkSa1|b5N}9v?JS_Xy2nsIPwgVi-F~GUY8a; zpt*H{##?+`>;A|2emt}nFw+2jCIa?p3%pBdD2fHo;AZ{d@NB_r9#Y@O!7;`*f724e%~y0<8>cBk2K6>G^qrNG;9H zIWj-*$enj`uS6*eFjx2-!Sy3Qk}=OsOlb4m^0Jy+TGDurlHPHss^Z^u z?&05BSa9rjxYz~2mp|Ucb0f>s_Np=`FRfJ|_X1q=i;IygEGSu8a+dJe+}vEm+XT_l z`nsB{-mc`&f931s(4p@T&wG|Q&TaUl5xn%-*ebX^jQz;KpW6nwcFdleQeTrOdPExS zFG|i^kvs1AqIuPkBatjFD*RFY;mAC;uwZroq})9KLVKa4|B&kaJ{kOd2Xc*NFaK!J ze_Dvmp50XUHo%8HH<(74$1i{RkghiF)M5BW9YvpqiQCoJLW1BE`2W8Ijm9g@-#mUi z)BP?zGwmckp>r|cQ>RS$(@6tf_j0kIV8ARn3p{x=X*{`h&lAjTZSC1Oc?$sAlHM=j z-)^;j#(WokCw@O}1YT^4EeOs1-VfCm1SxU@$_?|SFEBm06}wG+uW_vDqTglqFrQMb zD*5=whon&W*P`5)i>qEF*OO@d1!)KWRaE2Sl7Gd+vH&w|zrU4x?~Tmwr}sBtj_vLA z%MZ-(70Te1eZkcpYlYZU#hSwFZ7^rswU_8nzX{r7OWz0*1n<(@(1QH#{0-EC_ud<8 z?POa>fKFUReW>r}4GFSb`iYJq2-!T*cc=I7_dM2W{he@w13I#p7ueWw(QufyxU zQ=SHbpy!k7;WIxhcfMjpPT+yLita|lG{=jJJWffFi@oIpah!~4j*atmXLhd(9`6D( z!GlNbHWIoWvu)_#ejbhPO+p^jA+8&{34@zDSPD+%KR0^xNgZ zdk@R)RdwP6A8-u?M9~Imz4ORpWI@N{W<6s$Fui@UmzmH5Byfrram^m4^uA;BS$*tS zRtxrx#?R;*#fQ5x`5H9fPwSVyG^dZl^d3K+>6%{pfyr@N=+B_+1-Nd8<{2$%e{JCL z9tUTAI7^Ap_j&Zu=Op~tGfn=k_2ma|*GB!f+BmXG({n@{kV@Gg2CLE;d zkkS)pae3LA-acM)FTdG5pXLak?y?s(lD?lJPd#H&kYTuQem?sEukgS8!X&iZoAiOi z(&@ZWAD+u=xE}Y>_0S78JJTbe_sE3wS(%hS{j`>g`E~uiTw+oU@L*2p*F5;(b8vt@ zuMcEUzNYWL|DVM^FSfabm{t;IZMr7d>}3ygYo1G$Z#@UL zm0ujZ?JdxR*NS|;=E%NS7UFJPVlq3ML|_2j@lnSBZXOXD4%Dd!fVwZd_tgiWdlFKq zyiMNmj#rzDw^DKCp@(!AbSz#SEI^>p+@|-Ggk_LxAlFpmr8S($Me>L9^YQ(M9x@lf zDI6^C%U@Q9KlQ0^l3Q;1dpVFNFONNT2ejMg

8L+@G98S`oloK$>lNK2Eo#{J%c2 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 P00000NkvXXu0mjfe_T6_ literal 0 HcmV?d00001 diff --git a/images/icon-heart_.png b/images/icon-heart_.png new file mode 100644 index 0000000000000000000000000000000000000000..ec5a40171b336bc4f48283b90a84d6238d3121ff GIT binary patch literal 384 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V08C%aSW-L^Y)fu9+RPn>qW+g zcLO&6w<;}r%rq-ULF)GT&z8&Ge4f01uW+~!s0#=jQnyd#e|`S?Z~OK6`|Z8|?`yf; z$Nk?|@!f9ye=EM&zw^v!rL_bZ68M{IPiUZ}YvzBI9FB1hi> zEk+^hP2w|{gayuu+wSu8A&3`1-fY!i-h-QF8 cq+#(MeyMGNM%zyAbpQ!?y85}Sb4q9e05<88x&QzG literal 0 HcmV?d00001 diff --git a/images/icon-help.png b/images/icon-help.png new file mode 100644 index 0000000000000000000000000000000000000000..33908b6751567266d28adb8d23d47b7788c7ec3c GIT binary patch literal 1348 zcmV-K1-tr*P)Q> z0u~mTs&<@!IFkbw0z!@)h~dHs@*xNb1i}zL;lhOj4oDCok-&jp41olQfe;i_yNk5j z)3fv4HK3H#(rBi-x~jXn>Q#;B9jE^R4dA*nAz%QcClvClO7mx+=|`pMhzxlKq&@Pj z$z%M%$;mU8TklFs=^aY*ZwmQXrqP_UU5pV~ebv0G4JxFMGb}ajf&Fzv~vN?Z@>gyQFMV} zTyv$kj{8Jtu89y|JmyQ6^hS}NK6B6}6G7(oo8cP#0?F_p! zY0~hRxk&I&dKf{^rim_qG~am#X?NFof^F+ud7^=x zVlCcbE%bcSB0iFKxZHcyfNqy8T)lhd9ME(xAbo4`9J~|FSxNE%X`6K)x5YwY8&#&s zu#(WM1?C3^W>2#Ds^j-vqL2@?^ZICJ66M#!m6rUaTFsoE=stpN()g)DdcED{Bm{Rc z_95%EUQ7ksk@s~N9+)AQ@J*Jbn^Oq){2;tWA$=k)0mu(_H8wD}Sex`gnl%9F8O!}P zMukfy)yY&UB5W!W4q6v^R~}RVNUuZ~^*z@myvm{iCQy9yydekrXS z-+$m#>*Rh_NzY5F1&O1QwBv_8CLCLhfz%Y6&_nzxXLfNG%mYlZX;)Xiuf89q+0unU@Morb&3sVL+RvzC-j1CYLI zJIVcdWGLi!tPNdAyqD4?wv&$(*!?!ZM>7*if@x3EZuLnOB(08qxlk3uo9%!im0oHA z(&BD*i92>+u^)}C+PJwY+MT0=Qj%s8)NF-Eu*{-g=bL(^ylPf#j@F{VTmq%V9Wg?V zR`Yb(%KPH;j$c)P^hgo8(&(CtLzU9EDX#eR3Om}gZPrso?*wQFyH46^NJNYI&!W#>TkCXS`qQ3zFTb8P`vOw(s0000 + + + + + + + + diff --git a/images/icon-housekeeping.png b/images/icon-housekeeping.png new file mode 100644 index 0000000..62f5ab3 --- /dev/null +++ b/images/icon-housekeeping.png @@ -0,0 +1 @@ +PNG placeholder - 需要用设计工具导出实际PNG图标 \ No newline at end of file diff --git a/images/icon-info.png b/images/icon-info.png new file mode 100644 index 0000000000000000000000000000000000000000..e06476ea99594c1bb9573fd9e1d843d57825fe3c GIT binary patch literal 1123 zcmV-p1f2VcP)e&nR_fTF&Fs0WFO;6;t17}0<~!C-24NoIRy*x8xcWi?dN$z-Os zx~jXn`m1jZV^#hGIS%JHGDk87c#VU+&xL%&1b*QHf77661~@_AiaN?V(cQgarTsQb zPLFUQe{hhCRgL0rhA9}bRaeh*LIDSSQMJ?2<2iGsQ+vld@;f--;|v{N)W%pVc-w2@ zye04q2mBtL68M?{_Hn>2%jwzI3f`vuO0A(m&-9MIaln2Cax%&e1Nn{gvh>~LfV~lu zEpWgA8`P1iZ94KO6LPRMC7T*#ZD6A1Z$b>hyn6SP6kE=ry79l`!oL6pj( zzA|$h&@(cY9H3%=^C7!@IYrI~R{ewRs%E1`!W?yggPhjq%?WurNwv`qEPkZzHfk(w z#gr-!Lg1{`skX&7qL)fan_8}X*iHn~2_G8qFR8#`$~@;Q`T?i{sRVc`PRZ17*s&xI zFb8cWyv{W*{xBl~!#0Ky5_5|;V0(Od=bVOVI z+es$QDgY7&IHaA&AtMSB>!>op4V!5SkucY5(E^yaZMEL0ls<%Q>JD?m#bh9-{JvR^ zY7J;HA+KwZ_8AQ#4Dgm#6?+3pg-)tmDN{t4Ml1~#Fu;2OC29k;0QMRUBDSlVwE!jp zN(}I`Ux_e$nn{-qabY-UrwX7;ZPD(7Pay%wUw$QOL{bIdE2H$B#+GGWT&HEn&x!#6 zz}hsE>M)?@0dSBrex>&I3!57y06R_!>4%tl*oFzfjd6&xE~8lnz`^UTSVn{GW1$~y zhya*=kqLP`HyR|AqMJl{}3(2tM%7e9Xo;&JrIJKxCkwK!k13}Q%3Yo7-&Wqq*RSfV_8oNYv z^@r>PA;L$*+oX3q-=tR?-(uZJYrhag=uRvVU-T!1>$Ow~JIvAzJInVmmN;_ql!F0|R$Xn9 z2(iROv}WhH9On+6H0mrSVJG4$!~PnIvD))=eoEM8Wf?$%w}jED(O@9&G9h15k4;2D pi7c3*cl3>PNH-D^uZr<+$zRaQ<>e|R&XoWF002ovPDHLkV1m;(`_}*f literal 0 HcmV?d00001 diff --git a/images/icon-interest.png b/images/icon-interest.png new file mode 100644 index 0000000000000000000000000000000000000000..c914594e13b0f94395f605c4fc6af8ebc03780e9 GIT binary patch literal 7032 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 + + + + + + + + diff --git a/images/icon-listen.png b/images/icon-listen.png new file mode 100644 index 0000000..f28be6e --- /dev/null +++ b/images/icon-listen.png @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/images/icon-location-pin.png b/images/icon-location-pin.png new file mode 100644 index 0000000000000000000000000000000000000000..e1b065576c9e9c7cfcf5dd37ce41f3960dfb8144 GIT binary patch literal 1307 zcmV+$1?2jPP)J5?!CL6>dl%%gubdtNECl^=0p<`1&Ihgi9VPI113Z<{shU7cfP3*!5;`D!6dq# zQnM@W^mO0t%+B6zBp);^dv(=SJylZy)m$(70fU3@oh$X&DyeC&(m1U&{oUEZY5pIn$5yeAc?rIv@g4~@*?G|3 zWWH4z@0zo~pETw|scF3Xz>`Y?y&VK*K~IAp9$tTi0{X&R7J$1IP+jS0nOb8O2f>!} zc#Vv#zp5PKa;fndrD>19+;AW7(X-&)C4&v6XKJc;jYitTDnRDd3_8+NvuFW1b?3UHGGIxGQC z@QOccdWHWsTTe)5jDx{^(!)s3L_Ia}-djf#_lN(0MJa(Jx;a07h z%Ths&Zdhcqj0Akyc_<++w+`>C%n3C0{C`eZ&$tty!&cJ`_CQsMJG)hGtlUUMcp*9 zf@r0cz~|W$0NP@4x5+}I{3N(!c=5?QbdHxsl_u<3nJP_BS#|hjk%LTX+T1CHOQ0t* zCs3}LD{Wy_s+pULkP;H;W1p0eRl3oV5{*Ec%S9e2O`lk&Htw@+B{9)~n0s|bF{f#> zovg@O3?5ge5aITy#xGx zAsfgL?{hTdXd%u;JT#w8`3JhUDF3ZZ=mTqm(?dYFbnUllZncG@)bze68Z4(-)><_> z8LZv7Ft5*G?Z$hZyP4fPxevT%*`57< z-@KWfnH8c>pFW;QD^l|r3B(d;Ng=IDpsi-`O7j|1TsNmhcac^kPTrKjYYI7a3454HT1)UNn#@g>s9V6ozLGN_Om9>k~3phm2d zONEFZ)yWYB{Mny){@W9_Bm>J&E1lH5Lr=U4I=|yJM+0F zfEEH6$gh7}HVy_ZJIqF_{tX~+=Dt{&fxLEZ-+g${p(i)J8&JsKeHa)UGlv{{aw~*^ z^}H9mEC-s)klca1?%x2=g2RjF^4sATWewtKw|SfOun+psZvMF=F&v(N?<@>3Ct2&T>lH=ya9 z2#G{m4p%<398`Q}K-0Bg*$2KF+(2dm`K5mY^ro<57KL=hw}GhWlL>{q=+VHUD4Ix{ z6F!bsB=Apl$D9;p;J&jMjVNH9YXDe_?j@}# zL%3g~GZy%kAiWmgt^obm4enaxS9(qFF|&pT5uX;IM}oW~$o+`{{9BMat@^#QW)>jl z`I1H6no4}15bCv{anUPz3ynK}cQhaQAvfB{x4rHBDDoEhi;|KkSzN`{6B^BWV@bv8 zwNWBkRXd?M@1~6f`e~6@;y6A!Zk%4zdjxnpJ(j;eg!oit0tEPpvy0nOOU`8--$sgD z5rX{KRCiR;FXw^0nRlMuddub%7(s>h8I1^ zDI&mk9T#4hvSE?Fr525n#iDaH>y0JnS?Zdy;hsKg%kf;%xz?8BIVXj-OxbYH@mZU1 zDuMt95*)~hkU~|ZP~Y^Aw2`h`pv^&7RkUO-TxyecavOun_JSPGMP4gI3$$K2X&fCE z_;cnuN$VOxe(#&K>=QxjniW$8Sl}DJ$;!B>@wE&BXfvf8>A8)Pc~dcLl*|k8Gl$E^ zBDKy*;|a|=FE|f&Z|We|wn~^DCZ8bSkRibV&ye7NXGn0sGbA|R84?`u4Eg^Y0P3Y` za1Xt}aBG{toCe--TDhlMmz#>wtn-2edYQYS8GHrs-8$l!0KPI+fPTUdpikb(Vmd7H zn@mmp-cLN>cfLu>UeMdzX{>5nKvxCnL!lqp2Tm92>Drn$nRhV#P?U9GbL>V{E~|w&v7q6`JP|_4kyu?K%X-oI=DoelUS3p zlM+BYLdp-stU@}pM2uxrhm?rb2XrWjn2k(_kci!h^}G_Z3cPpJCZFIq(_VweX!Eo1 z0=0&;P>E3z_;V!`8etSKoV6{n&PIJ7=l2I4!h zM@$|=!wn?96f5vX56Cf)yRM(wbhv~PiGRi`HgUE9*9s*M00000NkvXXu0mjf0p7bd literal 0 HcmV?d00001 diff --git a/images/icon-love.png b/images/icon-love.png new file mode 100644 index 0000000000000000000000000000000000000000..d6b687337e3c74b62ec5b4d8f714e375200437fe GIT binary patch literal 6063 zcmV;g7f|SlP)x@ZakZBzVzCY<#ifwdIT9hP-hu>jSdO-l}ZXBKG1x8>*0r54fBZu(jY^90Wcs zxB{+<*G?4B+gtB3#^M2iGsU=O%yx0A@ z`@Qb(r)rE40}^G`MK|ZN}WziejGf+wrwo}ErKACfBpI}H5$9*3vERHQ;o_WWoX3+f|(~V zFGJpv|JEPMaL>z6%*$Xe5OjqA$1GRYFU=^%%+jV!7sZw>pArOW z9pIqC4h-z14(*Ze_ADTKYZG99X=xX={YgS-E6zV(r^;OE00-69^A5_H;zI~+?X-en_U6sUs(j5{$pEiZ zTOlW+0K%#~C9Co*RZOJ3=o^#S&8-rOm#bI)M73d%j=ep$#3Zp z0az=T;Iig5YDIv9Av1|-8Q`4+U_Wby)TIxhgGCuUWJY+w9XzC-m3K%+sbr-FIJn;J z__MgT3l18QcSz>sYq-?mbZG$&ZV{1i;$}HOGI6@hTTP{01IxegX-rl@wUjftRd|5q zgw+&zjeP`AIsSPW;t48HVF3e3~KvP-JFOD|3LC8_>;RPcML$1=_r6G;3E614C#J8}b44$?M^- z&QnGMl)>_14P5tvn#)&-uKsEDNA~$OqHo_Fethq@h_?R&(IeZ5zO#d5@=%tcbm}2=Fm(1Be;|NfI85(dWC-w$7G|BHCl1uh zQ?dg7KV|a$Cx~7c4=ZOk{3h}0yF@b|Qzl%M&%79p0*d5!J`}^lW0XRw0Ar~n^mXc& zL~s9=jNx#dJ+iSHn|hOoJFr{6VeF=2rJI%34sh7R84ETb0awH4PswY~Kaz1=fnV8A z^w`7U&-o34-zf4h{wUFj=cqsn)M-Dc+XO&Az&94p5zgm@Bu|~lC%kb-pOtSEe+jR5 z^Hg|<^?Pb*1sD@o(39z5vki(KPHS#>(7X>Uht2@qT==+p;bmOqFKfO5UJ zz`=4!T=zmZeBJxT`&3zZU1xX0_lB32##EoKUV+>GaRTUOcFyc9ux+mVDI8%GrQ@Gd zeX3G`QGKDODp%jgg^#E}BdTW+t5slBUr55Mo~m4ZBf;PEODc>{$5fuyH^8XA&=U`z z>*8Xef7cO}YvKMC7}Xbgi8dwF!IhlvZ?C5?Bi%E=!QtDezR=5+8U?uMH!Fo1>E0E% z<4+JkFH3)=+9>1i+NTO91~^>vj3tu*E6NBkSK%H_@RBRA*HD@T1E`I`yQmbNmw3WS za)3MjV+7DoSug4`l=gQO2a|rKYk=jgqj5}Lfs!2sZ+G%T~ZVPw29z#W0%Gw9)_b5+OPIm*`QOsxOZ#sCW+0R}yLpQv^f=6=;>wVaLX zGK^jm;9&L{Ko^Jig@2{WI{FM*VweU7M&qAYTY)kATrZ!089umW3_m{4uw9NDqx1D} z)$D$>+}Y>#I-NBcU3;_&FW&Hs99Rf8ih|0IE1wZ9{iXZ!_}=bA^6}jr;ViSt5hon} z!IVd#*oueFN35y75gsM4tjt_bA7`KDrjuMwrHfQT~#0dxY=QG()MX(KD7Q8oE z_RMp4VgKkBf4EwK8;xCZ2n|(Y>ePbZ4dCOALFO)$Z@!;g$z8^k*T(R&Mq^C=cZxq; z4X{sR1l7P@BDpG>Zu80^zWjSRsq-|^2Xna3m zl)~Fc0|Pq=+=lRG<5GB8cpbca4crl6xT!tGyoaWF1w9PrM!NO<`p2kf4^S7(*ELEZ2F7*15pWN2j0VfgnaH{L(q zdRP7@3hHt(yiAB-)Er&|V*<-vJam5ggxakfUYNiq-K(tY4esw=&~weo3u36bO<)CsSMcUGZ7 ztKgkRXJm-)jWu@TepqiENxxgC=nz*ayrJQNu=^w~ft|y{*?yZ7cq^RA^}HNZe(+LF z{`%cEH|UB2mG^MEdf^RjCCS@JVgvzpkjj}vd9CN=iLj=@t%G@oxQgLrLOkHf6b}=) zr{6(ayf^m|hc?*fHJq+$c$x2Sc2DFO$U=0e02Wt=_+W zuYv2+GH~EbbbzdScv&!n78pW9#nd6Z%TR&AK3R_OtWkIw0_>`O4dniup~Ga^(gwI@ z;pIC>9)X+g>2p%v6-PUAsNCP~rgq^CA;7NccPb!Ha$Qc@g|bXK+ae=14KG80U8Qm^ zuWfs8RlM=O*3;`8UWNd>iZ|u=I|<>^Rkx}zce(7>RV2O^yXN6#2(Zgk&gMP!%l`L_ zZNOV2RjM!Vd^)fj;aw)MncrbN;q$#8_n)=p<3H^=psTy5PV{eu%ZpR~=f4P6p#r;% zhtVo$^S<+2yN^EM&HXBUtY5(4nd>3Iu2Vgmm%CP8RoKB)G}X==ww%#x83fpMs%P`^ zb?~9rvmGI7T5z_K;!L@z#sn3E09S$P*}Pv|TkdUT!K4-s>3M=p1y5MndeB(O5a6mf zzF%!99fbH~xgqB8ayLyCykG^h&501;s^IoEy&BKO1zjdf3b9Uj+1`I9NQT=;RrAho zh)%wz_FP`gb@pigjS;5khBv~CHy(K?`H=A%Lx8L3lnn6OzfpTGhxn&I%s&`u!yCsz z=HWDEqgNonRi%1j1LI(vJ(6FDO@ud&1AO$^;zkY@Lx8J_S7-5YqpI5q^9r%)@J5Or z{#yzGc1hHaSru>UKlC&8-(xz&d4)H^x6b(ctuX}HW|yz7KCXPGb^>8mrb3)|cq296 z)PMlnhy%+jlrELD6)xP$Cj=l0+^5JN_Zkzv2zPQ%+kE&}G z#!4(({KwDi3NMS+POEHy&Ve=~rGNTj|8?HKbFhJ*0SIuJY#cUs!y*2`$?hS}GrSS5#Qho<@Ef2D zt%D3hfXm`acq1H+^&!>`FJGhbusO}vK$Vjgvz+MQr~!iF@ipb)*VV(z!$7xsja8BoT{3{MONMN0Ux?`FlV8!}RA0tL}Gp^-#Bz5a6N(Gpn!mZ=V{8 zCVQHOmkb6o%Q6(ZAJyBz#36p_IBDUv%dhMwFb_lMBBcp0UoSRuUhm5tJ@|G`E`RnD zX2NSzDl6%3(Gb2+>`F?k6jU5g_fOYAHm#db^rG*iM7YG5?qZ?k+3moF|?*GJ- zMMs`7`~4%l#??E(JOj%QezEdi(q1ciYzi;qwZAhTQ7-s4(TN|Joh(9lb5YO6?mU6Q zeVEtZK1n+9j_;AGo|ceD@cnJs+57aQPbT zN{SUt2=7KVEHAC0`nJ>wCZ7INzY-(78zH=wf(5WS=1Lsiyl`#E2i^gRtRsQ5a52w2r&niXLwC+ z)-M9WTQ~$*MNuJ+`aHj_F~e&MtWYZj0<0?U5bO3ywh1=kmxcp;UH&*s&|h95b~U^_ zWag!jSfEjs1>rB;Xps;P5U4R7Vs3?V_;t6!%faP+yxaf}RkuWb3jt2URETxEHI|Ks z%8xxvwEIy6*9!i;@E4aZlJAcZ=*b#lo@qYx-}c@b;?_evG4;^>MBC-8^5g%A(AviD ze8|_p)AGY{0)1&i{K1@F=L#OO@^vOxzF z_}(O{A8aIO%Hs=Rg7kE^BI<-UXmb7%!VBH`{->Utz-kd>leRriFFai4;WidVKoXf< z-q625ke$g8^DUaUj-c{FZ$W_D?Rf$-OoSM%RzcOBJ1xOKVl|dwXI$d68lmkMZJ z2M+V+gUnL=WCkH-3G$^A;(M{701|x?eGm(9cu@>FQwcHew*^QNE4Hfv#&vLUdWD$R zKp(+MEa+iy@M83#h<_g4IZD72R#!g@KYrw)u;T?lvX&TXJ{bLEV9hTHZ(7FqAVG03 zTm~)vhw-1SJzmtKX#xOy39%-lYlqO+uMd;g_yb{M1Avuy14I1hYbtPfKX`SD8USqK zZ&;$|(ZlPJ3Il0xPni0%I5rU?czh`tUNZ>vJ1ok^sOS zU53xWr<0zKHah^;ShIK|&P)6)UgHb_fCbhr-iS-Cz~N-pbpn8LH3vt85M8(XCRX6( z$*jcW13(evz4)f;V&%C%4h8&04Q4GRy;ogzqcwyMz63V>1LUE_)mJyb zdP8VhUL#GO@%zl`oH>G(cLZht84oL+HpiHciO?E)W3dSjaMTj${t7*s`UC zS!n=Gf(~aHI!CfsVB+&yww{syeT@J>fxKnZY)+8@nXiE(!2%D_;OpU?1OQ1gU-!lt zslY5);PH)3dkELV07+UVu6yH*1(*fb!=Csz0)UOSd~=V9>)tq{0cPQb2izY9Sj#^w zHg7h5=9uvSvtUxoYfKUVP_S5IXegJHPK-@rb>rVylHas{kN|2+&NvSZ<+kiN*8nsB z#^RLxVIKk1&v`lHJV80iJ-`ghEun^HWO(;b&T_d1-oS6~!J*+$8~j>t4o*-33JGv{ zP+EITemFw_RmKMb4h5Yx3Rw?P$PhXX*Thg^xnJ|HSIiJPCj1Lx-857U&o&nuURHR3 znHU;8g!{x$9l?FxQaaQWK7@|*Z`_oB>CR~aaF%~KuSFUvN)0gcZ{8fGJMC$ly9AEm zb^p&T_ew8K=>cZ`ty?4X)z`BKF*t#56AcZHo8Fj^Mk&|8>+vooF+4Pa$t|#-|Jw3x zqSC|5tf;_o{^H_c&|ygkEoDNR3-f zP&zrwJlwQq$h@AF6*vyn7)rra_pWaq%wrS2Nmf?ixYY?R?=bnkmG6R3DGMuA*YfZ( z>nm_vP>sL7JxN$g4Y}~oQ?q%fsEr6FI2GUshSgvc50@Kavg}0}X*e0+NN~%k<4+>U zkdhUvsj_f7z!3x)G_~X_|G25s5z17F07npH=*i)sa*|2~IN~oYjmaQS5GK2TRWP`b zN(MNxGHdPoW2nltvk!)tD=$6W$Zm8)wZ*eO)eYTHe10vz#u zf0&k*qA-sWxGoH>M^iL7I9=sx+fD5PyxvM>#xUC*ydqD~G5R&MDh~AsLY*rf4$H9m z^YY_)8Q7K_7SGnX!uEpzrw~-?J9kE8!ovs2V3Qb@FFtNUARjXLLJSd_5`@-lLJ_=( pf@g?1l&v779NbNt=22Po{SEbyIIdAXn}`4a002ovPDHLkV1oV7xWoVe literal 0 HcmV?d00001 diff --git a/images/icon-medical.png b/images/icon-medical.png new file mode 100644 index 0000000..62f5ab3 --- /dev/null +++ b/images/icon-medical.png @@ -0,0 +1 @@ +PNG placeholder - 需要用设计工具导出实际PNG图标 \ No newline at end of file diff --git a/images/icon-mic.png b/images/icon-mic.png new file mode 100644 index 0000000000000000000000000000000000000000..3706418450d0c845b7388bd7acbd74b9a56eb117 GIT binary patch literal 674 zcmV;T0$u%yP)jhGyC(LK4>gd6fUq|65O-k4;+v(Zao|#tW4-Lz7ZPmZJtE;DKqNu_f z126@w1OI?~8RqvlGZ%#lpdZ)<9(WmMZp+9<0g`W#9<899;Qz{Zi>)Z2nAc%*0yKa@ z84Y=+!)Y=E2h4$O$1yNuksAh1mF!wMtUHOB4AYKAf^1XC9u>>hPU=pP8cdL_E7{v( z+1g2Uz_)zvs2nI8)ee-6Y6r?j@H+s^0T(jndL`tN$6+sv196+ZxBv&@BzsvLa2fRN zKnE%ZP^fmGgb=z-dR=zSck?>U^Z6Se4(KXs+)>ap6Lifh*+Vryq2`Z$IPg;`opqFy zruH4qQc?n6)qE?Dj>BfkqP{61QJ0lJbrtnEY-M%ei&AfEZe}tf*PdS`RvFvTGVEUdfBG55QlGvAv|rD?y(L zn|+JE-@tqC4v5iF;LKw53Ru?JDPh$YEHVFdw#ITH-$r0sQmGBGq|{ltRR?5D$R)L7 zNtI7HE9?UZ@DcchKm752Xj7hvRr!RJ6?f*q<`(l^oQoZ~c+%opLTw}p1#*#nHm?Z4 zfra#eq}`dIJ}g(yrs0z>={4%dzZrvLx|07*qo IM6N<$f^pp}9smFU literal 0 HcmV?d00001 diff --git a/images/icon-more-dot.png b/images/icon-more-dot.png new file mode 100644 index 0000000000000000000000000000000000000000..92ae9bbf952788e2b91ca991ea5fb0bf1082dabb GIT binary patch literal 798 zcmV+(1L6FMP)0+KmEt~I2;8_#&!Am8_?SkQMoo0d5j9yS4AH6%{~ zWf@57k)-dSR18XaXz0?RzJaIm`TYjvHX#3q485r{Tm$0CY_@Q)cjOZjyMRhpfTWrj zIJJwv`^dL~AlQ>AdwO`}gh6@VJ8)^=UbC58cq|ck2#OgXUg#S%XgS~v@iW^%5LGCZ)?y@jK5ttn%bMDI5by=- zFceQ(*?C&x*#?1_GAL8(z$4or5LYNOUb6sqbp>8{hUAmGE5IK-mkuy!UjdROw;e!U z^Z@0fZ4ijBIX#76y1N4MTvuSmHVDKPKXm^f^Ql;H)`39s&1A08L9RO`@y07}_55JVP_tvR6OnH~zxmXvB*C_5DWJ6^l#$81Qx4u14yzEO#FGoN_e zHX!Vz(W|XRAbvXlkoB%YezRq3Q7tuGJmPF~x3#@KNSz8pyb}4gCLPG=z&L=V%aP#! ziz}wTmOd?~faFX6qb~EQTnJtgrXP=9F7$EFzt)=~FY$}+Tq!gtpL-c~pLjg+i|$^J z8W`fAy&pAbv(*Gp%8gV!*04uolI4kxFZ6ZTqnl|SrIp=kdAdGsqmf;WZtJ*>Or>*t cKB;k=Kk*OFfE{?mKL7v#07*qoM6N<$f?Q2+g8%>k literal 0 HcmV?d00001 diff --git a/images/icon-more.png b/images/icon-more.png new file mode 100644 index 0000000000000000000000000000000000000000..8d160cd3409cce8f591a50f440890c102cf14c6f GIT binary patch literal 239 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?3oVGw3ym^DWND7etm z#WAE}&fCkjTug=nM?c!s51|amV8a9GpS-z-L25&C(y0>;6HotzHpU2 z0ziEX3Uzh!G;Z1J)3m3?F23>hr_@F7l_f9ctgi38zR5jv)3#}I zR{wWf9`Gf`TklUq#R}Eur + + + + + + + diff --git a/images/icon-order-complete.png b/images/icon-order-complete.png new file mode 100644 index 0000000000000000000000000000000000000000..01e87eb0d722538406b88513df9b49e2346ef339 GIT binary patch literal 1236 zcmV;_1S|WAP)AL-NGSjy@ zztJJnRd>`7{KSPIG7CQ&cP>m8q7YGo5fl_rM08^`ppf7qZX^riXOb9=MvPG>Qr&ON z>-)Oj>wcepKFNU#Gw^QRQ@5+?)<>yR_#Xff&-4O&r$%~4Bkur1y8-DpK>h^`9hRk$ zzXGJ!0qqkIP;L3-kw5Gq@e}jwt~JqxeI*Ye2O9auJOW_&TWxrYXK1~T(RHJvljn$O`0MUJch#zm z=RshXf{8W&@^gT+-!Z3I@uODf`$%JQM!W*5l_QV*WHxE~Tb$Ml>4q|PJTo(Mnhx!) z8tHj3{HjK}y7l?dYK`>gzvm+DFHBw=emchq-}l3*%T=EN4*60FWZt~t-vRl-Dk2AD zkeC0(oZD{J0gYjil~q?OL?&v`*~-K`B+$-&!=qv2YK>`f_&Di3rJhlu#gudg-Pn+ zx>pqqyN343@{#Xu_j})1%(8hvyROwfFPORp41FL={%$5>=!-_7HMBPtQ9vVq)8O>S zTjeQv4hwC_$NZ4?Hdx2rEF!5~|BkWyE-0n|edk23OeMM@FK{1MI>o=WUTsVca&{>ffb?2A zj9OA1AWw--$qO`6nlS7JLou#nOdy1RSV^_FC0v0NN|ufk{&8KCq~C z_;MXc1o!hxc!6EvhVqz05RG>vQ07^g&0S+R_qbjjNo?M2DCp%a*2sw6&_CNn59s99m?^4A)9k4z-RImJg1A6_tM-lw2?fPVlq^EX3jv>W#T0000JE`%a z;!WkG4}mX$+%(aw5^SMOTc|q__Lt0|Bul?-xK%W7yI zE2L+@u88E_d823!V&0hfW236Sba^V?4}=y0tvps;-mNFR}F%5ZHA%!*$t11dqhc@B{3 z;s(q*p04R8H}Z-%zk50Oe*;LvG-1n%7clTAunh5b3^cisB(x0i);{9o*1#SE@*m>oKROa{|X z1LS*Y(J&3VTVxqm6cQ_>$N8>}g&9X4nRic?O}?3TI5v=PxY`%J;R^uk00000NkvXX Hu0mjf>+s$% literal 0 HcmV?d00001 diff --git a/images/icon-order.png b/images/icon-order.png new file mode 100644 index 0000000000000000000000000000000000000000..5ccc23957c1fe98882cef1d40099be2659ebc3b1 GIT binary patch literal 445 zcmV;u0Yd(XP)?eV}Hhv=H2}Ie2PjQxWtP@=%H= z|J;h+dLqD{#mr_2&IfzV{=WRO8yI60MQLJy)-8eD16ucr0Bgyp65G2?ykG=?z;(c??~x!YXKlW={h`0Hu3e%m);Ct zPLp)c2vHXrB_)t}U4G!{zEt=+0ibj)DpB1)GJMnXBVt>X`16?vAs{LdNXans)Jp+M3Z6GkH4Fm?Yfxw_P5E#@30)yH>{~F+5 z=Qdq2OBSP&faMplxs7keaW+KatIAwx<$Z4Ba{;mQKDY6=__~;6c%R$&3O|msq3L37 nBN%#J>Kt>3(&{6jtg*RSF2Q~8>n00000NkvXXu0mjfJyXGb literal 0 HcmV?d00001 diff --git a/images/icon-outdoor.png b/images/icon-outdoor.png new file mode 100644 index 0000000000000000000000000000000000000000..e3f01635db8135b1f808439299d12ffeb2734c06 GIT binary patch literal 7796 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 M07*qoM6N<$f^9CIUjP6A literal 0 HcmV?d00001 diff --git a/images/icon-phone.png b/images/icon-phone.png new file mode 100644 index 0000000000000000000000000000000000000000..8e86d197f7dbb4a82e794a30957ac474657b764d GIT binary patch literal 933 zcmV;W16urvP)v2O2}gu0iXaFP`Q;Z*8ua3iZV4~Zkx(I zoqfdy^7(_Tj+nPy<=t~}09;Z+&P2>BQ+Y45uhsylS3-Is<{e4O7nlJI2Za0?tG^M` z!JPQ^0uRuo#2k0y6aN{Q<>CTpG+N~XBd*R6@BgleN4ek+CsIO#)EjXR4e=>)u>hP` z;<}8OD=NlsW^-K<0G7xC4zf|jlmbI4rrjk4phJm!qjzTDAgMIEBmk5fi*%og$p;>( zm|wtVmjp;{vHNw2lvMx6f=n?jyD zM_P~vz$@Us+%Z*7=y}2~T%cPC8?p}kA^>Ae{I{@y4Zs(|>5~?{30Rl$MFFtTm`d;c z6jO0|AtpT6{XP-WevHMS7)t-%3D{{o_?f?6F=BUzh#W{my#6vPC;H~DBcxZV0lqOh z!!k{LCEF%%Ssza|AFY?>#&Q1D8=U7RP-Mg}GP>KK`1+7Bs^qq##khC!G^b@i1}KuL zqj?IH81a6yJD}Q*aW#IxNi|k?+6Lr-CBQqwg#X{-kepI!0DTu*mqv#iWFR90^3a+7 z>V;H(`Zu2O017>bn+Rk&`T1{g&6`ZLfxx1>7-Hg+l+-CMsRl*>b{S3G2tB}Bmt+Hh zvT0SQ5$^cjD4kx1To?#UsbkW}h4I1PegnmcGY}YXzTEWv9>vJ89EgqrOUL-KLB3|) zKw$6+X_ceU3)}+E%9>gMtTCo#XU+@+j4YGZ!0!{8vLojP0!E7Ekn|!=@%{_txg@~9 zbiXPe*#hDlSxt2 z*O^3xNEtRrvfj~n;KpdqwWD@f!@9La+kc)A+nqCYw<)v6U(UN*?=8F3!FUF$BJjXl mepai$#pN#b>zNs}4l;^nZq+k=x@j-a2@IaDelF{r5}E)4(M1pd literal 0 HcmV?d00001 diff --git a/images/icon-promote.png b/images/icon-promote.png new file mode 100644 index 0000000000000000000000000000000000000000..576b94462903ef325b96694722d32b14b18e77f0 GIT binary patch literal 718 zcmV;<0x|uGP)byr{IKcHZ7g`kb)wpzL8Oh5}2 zvC`7gAC;xzHCx=gyf=ceiG`h@yTuBwtq7;NRJY>FN#td3HOOv$IXkzJeDEM-X5M_? z`(9><<1}c{puv9&<}X{zAI|MXJ9jE`SRwO5g=0=q?B%kob8*4zE6s zdz-yDeE%i>5q(R7pL?6#WbC&K5+fz(wG#9I1YPu`ce2y!95za{)9M`M#JmA-G~N(* zXI1va?U!-%F^qrij;2O52$MM(?u?YAJ_vdWg05{zJmX*YPgEkWB;E%}dqvxVtD8kW zEh6m%J})XD>~GJXd|45rXjoVP z&$U9njEGVKc82!I7ZqW!g?!`Wi-_QJ-oQuf&y9SZh~yX@{L?Y&&z*dph~y$0RJzD~ zYvd=uHZIS)(6~jo&-ugV_}br2-p#teu#L-(ZEU%Cy2NFa8QvA|lyw10=(Z^VAnEb} z2*5|?1mHvK0zlF$V*{)_ts3nAI0+Y4tO@X>KA%TO348-WpXErN z^X9vB&YA#_cr&A)KX^j7VkbT{na|Jsj>j?t->@fufBF607*qoM6N<$f*R0L AcmMzZ literal 0 HcmV?d00001 diff --git a/images/icon-refresh.png b/images/icon-refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..72e389a30f0161b51e29a1a4f26d4d9725db4163 GIT binary patch literal 1026 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 literal 0 HcmV?d00001 diff --git a/images/icon-settings.png b/images/icon-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..f7ee37d854a37c20496df5724fde65c756d63eda GIT binary patch literal 1452 zcmV;d1ylNoP)j|AY+*bU*>#C$b1d*W9Eu{uRai%)o<=OSg(F0RYTVfZxV5|KAuN85uc2WCf(_ z-Pv%;8vAm&krR2;RBOt=rU>SCkrj~IU_jS3z07c zroptB=0Y%M&x&yg_=E!79lUz{z4m>6K~s+eoUj<1DuTJp#;3ZVH`DFqf&c= zbtl0z72s!<5F6qJ(74Akr1|1#rY8_I>ex-}WqclIn$v_dLEQQ&-72;LO#sk6#jOqUUX* zO5eAQrs%!h^kLhl3Th6wS3e+}c7K$8~DADI`?3bgl>FIIs2{0Q^i zLswV=1X2z@?I?T&z)tIQ#{=_SgI8J1?^)90T_|P)Qsd@8G_+1b0`2wBv&ux_a{7kP zvTk5+!>tGX{wK3yK1=}Rngz#<0QpWc&t`B5;FYbakScjyX;es7XeabMHgI9V24o@J zZW}QhqQ%r!HrP-j*7UvQ*H`)1FzyhVlLeO+redC>Z&*Z?fGd)?QD&44mQ|*rHhl6n z^HFY8<8T9AjnkayCHFj`fsEdZ3S=CV(grdK^il*1ZA#<&A%Sl{^OofFSJ@}U<=&x9 zCGewWsRVGJNz=+|Yce`+BueHK&^@u6>OyE*6IH0x4}(uWX)@<%?6U;_^{}VYtk-;t zT*p>}AnxPe{4Gr9n?Wry2M#1jIvprzoKpElag;!o%q`Xp?uy-{G;M5FQYt`CY5bse zs++kiz~3TuTjOdj*bhZf zuQaZgKtI@XiQ_@5Z53{r#I{uczib7!1R51-8e3H(Y>CjDQFs;5+b!YLw56kC#d#tP za9@&3$&eS9o!nY`1 z4HQavm#ydAkd+$04(KYNX{EUf>?B;XfN4qa7I7LPjs$S4JhScf!}Y+y?pS06REh4e z)Z+j*2Dm+PvoR-eI$}Qna{pgs1!{Txw&b1)Co=WO$H1>D&AM_k>V?xBEJYSUg6VDg z&|P`m@w!Epp#VddC{3?c`#bf2HK0ie^h%WdP)PGwlgMv=p4NTj~yD3^h zJGn3DqSc*C{RLJ^yO6r_FSyihr2YY|PRE5CmkL5>Koq5NGYFH{Bu?T?KI|NL5RwaX zZgTE@=iL!vflD%6&)Ki6oqrg`x-px!m*RCj2mX#S#K zuwnAEvo}wearX5)CnJ(r6-6WHHiM*Ge=aW8Mw&OIv?Gd!M7=jis?lgo$HiKU%g2o` zuqTSfe~2~Amw79~>GXpcQE*vKMQx)b^WiXIih+(x2<5pDF!{O!|#w3%cVV8|s=ujNL(R(o*7M)D=nml!hNL3y8sPk>%# zwi0RGqC~nZm42H*=(*ioIve;QrPs|}`JkE8YnjNWb&FRQZk>@x&ysX*xgMCmQ+H^F zi?g3J7dUnKn(t-Sd|J0CQGW!bzr{TJY9rmx)lJGQZqXzjd^y_WLKanUix8H}Hn&bu zdbg;8f*y?R@@B=GmZ*YT1jk=7^!{EH4OxA&rK_C)q>zwo>bqr>(5lf@b`#vkL@zYhvJ~Bmi=E6y$ALrTcA;dm(JXGa zQ;ON>DBP|rK^y%iw=46UjgJ0rR?0pb9T&zv8_f&#am+?15c%OZqN3UwJxkFP00000 LNkvXXu0mjf37lu$ literal 0 HcmV?d00001 diff --git a/images/icon-star.png b/images/icon-star.png new file mode 100644 index 0000000000000000000000000000000000000000..9fd7c41f454ad019838f97af09accf473c93ae4f GIT binary patch literal 841 zcmV-P1GfB$P)n{=iK!NPway`rcH z3Tf=p*~Nem0%;OBg2~Nn5+a5~u}FfAr4|ZmynF9X5hYECtjt}aiF@bmUEbdANj?}@ zmfe}}o0+#e?*Sa*KShzfD@JfbO|PMyP@0ERhVhFP3e@yk??Jo33JvX&-(y424#+RU zam#4K093KljPx*%<^oVin<{H|qgL+Y~6_$jgg#{7pYWFRdtKHiL;{oOAT-hedV?L44i z{MfJn#?M;0M2VMXy^EG{xKZ+ZT4`8)sIOQ^2Fq~FLSmTS7^W>(B%2n;v{mM!oGRP~m1{_V zVVp{utc|{Yra}U&&}P!aY;@bKQURJv0kI%rTg`<8eDhDAF^p?%&^~L*wdz9FNzpJZ zCr#4UG<;ciXIB}C=~)Uh|042+NT)?B^fWc(1y>s0M^r`x$|X(SGA%Sc_~{SBGCoPJ zZOA|HycE2GPAfRwdI31Cu@6Lk-%KPpS*5%#S|q5_^OKQhpvWS!aMUtR#NYEcp|Kzi zSfSn)v`;U@1;cnf_Ju2WEj9sG=urzQFcO;p!}z=f6UutU&1GE2epj*lRG0c)&!Z&X11zEXw10)%eWeufSzVBjh5Gq z8q;V)Cmf8cPRR!WbQq!U5igKh;fKRz#seqqySYZ TeNhZ~00000NkvXXu0mjfMpS@v literal 0 HcmV?d00001 diff --git a/images/icon-test.png b/images/icon-test.png new file mode 100644 index 0000000000000000000000000000000000000000..5eae09a6c146a58f00fcd0fa3ed9c13f13acf6d9 GIT binary patch literal 622 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!oCO|{#S9GG!XV7ZFl&wk0|S$S zr;B4q#hkaZ9rHvC1zgXvZDh%7aCsoI!E>E(QOg^LQ0A0_OoC#{zSsSi>|<_;VQMp0 zP%=5GwA)Rs!}ZGAuH|=M7Ji-|vODl*UGO#=sc?=K0jCr7j3U+oGEN&?9x!z+>)0{t z^`$Fz$4pbii~s!k!O60GvA#+0j9cOy+curpIMcTMQO+AzUV-$&+`Ugif^6rmm{UKg zDpW{hzIlS+;kE}Qv6j}0{r^o{G%3qE^1^-L{X6B<$|{$*@BZBU^KSr0;X1R;;)U;I zLrZ4NxpHLRcGd18r-cq1u76xw@9n=VVymk5%A$46;T0D)o(+3>(&}5b#+;njixk+o zPU)<;xBk7;$8YC(L*Gtcxp(&a^Y2?5J!Q+c=&~JjJpKH>I{zH2^G6EZg;SF3qu+^( z`(D1o$vnwQkM(667t?l$3J?CEE@?LXE`3BEG_dU(!Sj?>#xHfF1dc->2zw))xKWvVw*r-(>9 zJ9$o3zVSMzJ*dXR?^N3^>4c>DyZE+hmO8YTMxGJt_jz+X<>`_8{k69b=Pt^980e>Z zb@K#fj%^ASlGTAXoeDNp?5vO}3ijM8>t7T6J1gt?(U9(2m2-;c{Em`62aHG$jfwYz Y(=vWBue$XLn1C2OUHx3vIVCg!07J40T>t<8 literal 0 HcmV?d00001 diff --git a/images/icon-travel.png b/images/icon-travel.png new file mode 100644 index 0000000000000000000000000000000000000000..da7781fb086e71073dfe96c62d196d4fcd43af70 GIT binary patch literal 7774 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 literal 0 HcmV?d00001 diff --git a/images/icon-trending-up.png b/images/icon-trending-up.png new file mode 100644 index 0000000000000000000000000000000000000000..00a2cb4c84a647e986c84ebab54d4e59a9ef699e GIT binary patch literal 511 zcmVZagmeHZyg|A3fCOwnCm<(Oae@hg#DQbW1|1=6&}&PRhaD^7FR|m8 zR7mwFMUE`Jr!V$m|DkH_<`K{x=(RWgEIaMn5?&r-hNEBV^HU^06Nk!4E&3T@(O zPuKi@5;N!<_8!^FsW^z#&^I}z&;jD=_02U)X5QL z<6>J3lCE-%f+jinYmvzfl983%uM-E?wSsNr$|h9$t_f=91Q}`p) z3KCmYGY)cn@Yy66&k^L}w=m3}T(-pH@pwE(#s@v1VlKmAV@Uu2002ovPDHLkV1h__ B)%E}Y literal 0 HcmV?d00001 diff --git a/images/icon-users.png b/images/icon-users.png new file mode 100644 index 0000000000000000000000000000000000000000..877b89e791627143b794cc7456a6213e2ca641b2 GIT binary patch literal 904 zcmV;319$w1P)fZNo8qrf{+uKBaoc~ zk~%YE@sSuwBMIWTl2--Ah&QdLnQ6Ko0RQnwaGFk8zF^J3EM%wQ*a+5%m{k+Qskjjk z42bB4U+rD%=XINi>;#C0Nxa+wo!4J#xZ!^kA8Uz3CBWO}SA4i2&PGHhjTZ-&MhL{+ zrizn5tRS<#%Q^FFP&VS_5$GI!t>6a9r_XZM%JPAnBd4N9&%BOMG)(2?5ePVGiT$WV zt`3M1C*Q5A7top# ziLzyR{b|y~&U@RdgXIp=cux*y9kpDwd&8b^6Gp3CrTBJB?>|hjGM};}(Aa;NqM%P< z8gXX+!xRO5%1mHR|6z*UK4o6&l>fj=WPDCSf2%>u$-Q!kCW zna9M_5V&M9$y?tR`-aCS;61FMztNojvde~&w|NIv1b(@9J!^DHT3N4|o43xtbWLOb eiNBTqE&K+yJmL$YUN$@c0000mH~<6@{#_&`iZ0+NIn7rK!c@?a^?(hLIOSa%$B(&?G>B$H&y;#752GP&J- zPUm*_y>}dr9sEZCup2-FfM)>002b61R-c_98;2bMpdLUB`9U_v9CiVKT>v@&WFQW* zZKuAKAiN*IVF2d=v;w$~!f{U>gT|_(u}ew32EZKvtAXVpCDq|!ADP>F@G!^6XS#GnQ zUdPO%j~H$q5hg3zhMPx}v1twl#G$4T0e#uzH!yz1Z8>p?Y40w(c_RAElCNS<8(N)G zlb6&+b1@(;Rb~VnVtu|8ZX+4E6K7AsV?MZ*94P1zDJs&0m}FcKa+FbEkswJ z{CL}^gAZne$ykk|^?|XDZ zi6dO{MpXzK1#{O7$=f`fzAIdcv}rxmioAYlaRcF@NXbfz;h_kRJo(ZA(*iVQ^&F6Y(k7#m zqLAV)U5s=vBt9gSF;by=8a>b+mDf!S4yLxEasWxCFd+w)Hw?Cc@sx$E%_#ZmLgyB z4Fb5Rj&;yf1=`}*tetEWn+IguU6I$UdZ1bjfVcp8O0OCmb_D)JvL^vN2M|$jQ1a_f l0A8s1#-2*^+=0VF{01NBS3rlf6eA_2t8&^<6NjRtuF7ewDy?u*T2hsG zd{NnfRe2mE8Di!KYUHoP>nO3sK(bu};(?NC(KUHf<-4yY2ICfu|D-DSS0tWvHZG4{ zEGlQBsX5#LTGkS~P_!_qwon76n%oe#P+W#on=pXuy#o3B`@tD%1N+(0`9E-dGs`)z zP6I{?e?8O&_|giNcd}=Ln3FvN_TUNjJ*0bWga+>H1`5AG0%1iKhR?Iy^w?FvS`vyN zU29mLu2~4aql0;~HKH@1CBes^*biHe(sOc35YPARThxH{-~iV2t;4aSW-L^Y)InAG4tVL*Vz0 z-|;sC#U?7bh`IQ!vi};kLFv_`^ql4h?@212Af(nY_0GL&`+a>KU*><`zb-W8M}6&m z$6E})k8arQ>LH!L#e9H=!I2?Cl9)<{>1e zfT|{K8VCFikoeVAov#rw4C5<64_E+a;7<3Hz)V5_CcvKb6d}d;fdfjA0D&cnkN|$<*um;AGv49rJT5|#26uAo!B5(w5T3gp7?uo{)Mj{qUcVdaqp#b00000NkvXXu0mjf?P7~j literal 0 HcmV?d00001 diff --git a/images/service-arrow-right.png b/images/service-arrow-right.png new file mode 100644 index 0000000000000000000000000000000000000000..86cdff9e439220ff577bc808599622b1028f7b57 GIT binary patch literal 483 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

i!1r-PFHv<) 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 literal 0 HcmV?d00001 diff --git a/images/service-type-eldercare.png b/images/service-type-eldercare.png new file mode 100644 index 0000000000000000000000000000000000000000..9f56707d23ca3228d18360be4e2c2fd668b9246c GIT binary patch literal 23428 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 literal 0 HcmV?d00001 diff --git a/images/tab-chat-active.png b/images/tab-chat-active.png new file mode 100644 index 0000000000000000000000000000000000000000..128513a9e2f8d3c1bed8717abb3cbcaf1339f988 GIT binary patch literal 934 zcmV;X16lluP)4T|LJtRZFM5- z5EHlT93%J$AZ&q^@lAB6#)V)6ys!{+Dw;NDW>hYxnMS6&s-60h+Ds~S{`_^irY^^r zKp+qZ1OkCTAP}eyX{|P3qiBSI+ZLF#z{4K5$=29WG$OG~Rp5I4vL#wz;FBdv%V?cg z5_8!2N;*1v$^wfF{4C2D#VCorkVer-;}cx3U$nqmSH|pXz#9gJj4$Reu;tv8{n@nT z+@!SqS!HGp6MeQsOAO31QM0EFm~D;Sj**#3PCqvbEM^$-*?P8#&Pi--;pRBLRJI-?{}x*2xG-m@Nldh%Oxk>@Leg4o zfQdGh*}7vKW25MvGI5)wZYFIUU(*)kGS-}Kq)BpuiM}XVbhR`QOms_G5SNT6SfUjr zyKj^y0C=Ee&=cbc49qAKI8~;AX=R3{jVCzQ$F2qVu8&;{@LeCf7T~)+b}hhneVkK3 zYfl0^W}-Dqv?_;8`K{=Fl`)8kn#y*dS$Mw+8MLPK`%rklsu*O6R+TOP`=a|*#vs-{ z3E)xT{iF*8+Uk$F2qVu8&;{@LiuJno!iMV*x1kQYwvoYZS1h9w%$_~fO{6|LbY zz%Glb1{I|W%6RS`d7&>$T74YsL;Y@V> zfQ-mrKo!Rqj1Mp{B*jXHG-+b*I4N-20?S7+=6B|JW6rKm0+`QR#JP9t-=nON*mK?$ zaqdKlm87amE056(>P6PHjFri8^>6jBn@QQd%CUrj7n(tNvWAJCYc-0kLUK9s#PJo0 zWvUEor|a&0uTA7I6xZdvnfZ0wBb7dWRueU&wbHK^~hCa*!ALU8A|0N!0e*!E^| zS^nltzmq1(pd2syey!`J_f_iN_pEmo-n*I7 zF!|$|`4?tO@HYhtJk>iWVr{tL%7)!LtnWVl=KRaa>aM>2RolC*FQ1u+3y{@!a;u_V^O>W2}X$ za+l3gw*A>BXu3{xZTsq)*k{+w-W^ff-hD0kN%lh5KJMbanJ#qVq@5s z_$b{=mTis6X$mXM*3TL$0&Pw|itm9wD`U8WA}XIAN{|2N5$VLr6w^A`WOM*)}iPugSLmk~D0e2<6BylGWz51Y9sACZ|E zy>s>7XJb&XZUsTyyQ&t(t3r*-OOpKrZe`3G9Nq56RA6pw>o@4NI^>bP0l+XkK0WGk> literal 0 HcmV?d00001 diff --git a/images/tab-companion-active.png b/images/tab-companion-active.png new file mode 100644 index 0000000000000000000000000000000000000000..23561b9531a5c348e2dd1d7c2731ee495a302d3b GIT binary patch literal 2908 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 literal 0 HcmV?d00001 diff --git a/images/tab-compass.png b/images/tab-compass.png new file mode 100644 index 0000000000000000000000000000000000000000..eae081a1fb9ed79b85c7065ade182affcaa146e3 GIT binary patch literal 1717 zcmV;m21@yfP)}8RTQp>24!%0Ow_~(ZWt6pd=N!k^QOcfphhKc zjyuV?L^|tCo!gzrxS#`p$@D$v78Z^#J_JoL;Lq z=H*g<^D{w@^KbaK&i?@l)V=89TPW}=6!;ejYQ(wtw|ox?dXs9j*7+|%?vcw8a0>$d z8do441qS#t33`Xub`~q@)%wjO=wk#tYfCViYZe85f<0y>#sbEgpSdlFPffgg|{m#^S&{_F<=I-Ov>6DaTjr`J%aR4xmg z@A(cJxJZHyHltKk-Whj&<*L<6sFx7%>tKy2hkhOfZtj`L=U4ot2 zuR(%#bkZyosF*jWo2XaoeL?t*a-!Sz!rrTG)0V^X8WvQfuLJtBYktXR&50Jyq55F` zmc04!YTj)GG#MqN!ZoSFJ_qnq!x`lg0`8osOk7?RKTHb&hnTiONK~j#BA~ZT4w6#@ zr%O;mJ>3lXdLeBpER>@zS}|i)deZ4KP~dtKf1c~zU&JBsq`|D7ae4&Ol^b;ChJe3~ zRz|PPjW7c4G?>;dr^gT+I(4SDHMiUf^&$08kq$X7m?uMYo8=OUb%F@^iAH75N|3iZ zJqiN4RrgRim9a`lfx6y6Xq!_Yu!=V|O$TTS1^$kJPjtG61P$u6$QS-HdyD~Y(Fo^^ z@A}s|1wdBi!z5@N0WXC7$yBF%nMYk(!nvb022oS&;@rx~KVoh~z#|c-N9g)V&U2cLk+o}Mnrq<+hY6d!xdEqdf8m$d#K%Gh^a~gah zNhOhhCNx^KIXmM(2ih2PslPBY3WT9aA;y!?l0v?!S$BV?4Kx~!CA#iIl46$$oR-8e zBmUlPrZ*u6&5k<>J#_Id&H6K$lGrhKY^7$=Q&}@()*J-*A+mQ9R+Ahgy`rihqSm|Zb36!@Gi zC!Hi31*yS!*3h22lUABA&#mH=%yo=L#;iRgz8kEk8p>6xldKbH#)4?n9CV16wNKmVmMEd_ww`J*blp=P zBybWS>`yrH)S6L5x{(zK_?q=(mBC?9vw?$7OlYH#`Qn<*7d)G2yAz56zsaZEQES#| zQWwmO?U%3_@m!E0-N*`4!3+i7V}$?-`bD#;9kK2OnkveS5*6-&j^A2Y;6HwJS#3`s=_+YtT@t5+5tucK7nk?c7KsCBU z7p>+!u>CARryHnO>wQr|;&T!o^W&VjlY-X_-s?CQC7iCm;NrJ5No#KN(-JQB+ZcTd z63;dlJSLb_LC5yxAa4n8}q>!(Lt#|sPP8BK1$klf~Sar(gHy}2I(ZDNck zLt0ufo2Ot9@crC!t0LeFe_8Od-7#yz0XoWSS}$328>*((1nAQKCeN5+zDFJ&b<=RK9_apTvFC00000 LNkvXXu0mjfd9WY3 literal 0 HcmV?d00001 diff --git a/images/tab-heart-active.png b/images/tab-heart-active.png new file mode 100644 index 0000000000000000000000000000000000000000..55e26685c89f32946392db6c33990e11743daff1 GIT binary patch literal 1015 zcmV(Vv9~;-W<%6h_e>YIWbczd7Z|Jf9%u zdoRzKxpVG&^MNm1?m7JKe7|?joipdoC{-vF3WY+UP$(2yfVXyqGv@g|sU11)fPIeK zE#wYo%;s>sVeQS+waDEwwa5Efcf)5kshyM_u-=h-1UM^zIMUB10h|Em9I(rGyARG+ z+i%_akR#s`^1P7MJherDFG6@dYada$>2*5m1o%M6{#*^8)ezYe@_ldZ%G+yutyc+o zD4bKXc3;TD9$1?t90%+YV6sl*?_lOH0k+?)E#zzAy-2k~z&i&PjJI}~13rn-_&b<6 z;>ZtE)OO^t(Dvlv2=H|x1IZ=8SAJ~l{xsnzwZs7@a+{2x&w`ie0XcG_1?FODf7EcM z$j9azzK2O+gqyNXC2{_O= zAV*%#Z6u*CH410a+DBVpBB{bWvUWffxh1!O_*b*Bh{43m{(e0B#A0xUTPeWpP0DE0atRHi7w!WXipXecq6-^ z$P#15R4jbxMdZcYrXuQ6@T6i=Eue{OMZT<3Amr)X<|62nN`>FH?O_3aZGp*J3ios# zSff%>8S|n5{|sF?z`w~Zt=kduy+!%_`TycaM(!2z^C1f7jJI~_zyb>R(b45Xemex= zT*$Vt%3HhI0pI5iVO9ZG!oZ?((zo^rA^&J$&-uMiKCaq;0ME23q!5mDyHBY$AuS1M zdS#l{*dH$wWegfjO?%bRH1$%LL(^ZSB^ByTx03pCeC34Cqv{<-ED@)-H9x z(Fi}tM+ZLf927qM5eXb@a9JHpvc|b>ICAgZEDW7FcduGZ2)Sdv?M*Y`RN-8;7^$=# zool;OIM!(O#X`W{*g^Q8Nv@E8JFQ>H-M&^Jz$Qmty(OTlQ5JbGB)rzEruVp#vaRq-f-N(%X*b9gH#krJpF~?UL`yX5+Ffk6pf{9WLhmqiy+?+TYUA9)g2= z#>_EcZ+IXqyp)V@EYE`QTo&3)JSetj?i{xK;V2CHnk~P=#4BLoFmW#vzXZa@#0|Cl zjcAExU3-PpC3QCaJ>i3a(!c?kIHy{zPGaI0W1i5@uz0LeuACC5Q*kP%!J-U{9W6L! z2M8|8m>H+-lYc4*kH-y9Cay?ZQg;wrE=5$hyB27j@X%KDg6`lb5HO0N;d`!kWGmV- zJ4%3#(R|dT33_m1#PmHBxWP?HwAu3AOuS2J#4aYjGm+gc=4lg&%y6FT%}ApC--;|= zP@FT&4qu;=ewU(uM;h9`w~ zt&H`JtCV+BuPg<`Cbs-mgw2;{QYkE6ji~S;(_w({h-xcgb*5)3taMqqGY{P0YSSUe z>R7$UCv49AEX#NOOCoL|I4yTDT#?wi0&XFD^6HC@Mm}UtB5_=*H{4&eoCJ!Ly0c``}WYYKjnCrP?W7KYZ(s-lL~Hg@<7(@A)7P9omImd{G7uD}USm$RIRUb`J-oMSo>ug9AW zi=QG^u)n~;MM>+BEZ4|7I@viR1m)oD+W6NSFvKojY1#yYt0I#+30vg>nLL0pW@^a- zBijpJXmp&~Ex$4UC)o1*X0JCTSvO4xn0QW;*PVtEWnWAL0(pZ_ z+vKLScbgD+t~U!7oAz?hrc{!=ClZ75hT)!961wE`c-vD@P*6}%P*6}%VB+{2koyOY T4Ee{h00000NkvXXu0mjfeX{Gf literal 0 HcmV?d00001 diff --git a/images/tab-listen-active.png b/images/tab-listen-active.png new file mode 100644 index 0000000000000000000000000000000000000000..e7ecc04568bb623e2e9020fbc0bd97d423898609 GIT binary patch literal 750 zcmVdeC3U1(GZhy9KPyl!2 z_F;5(KrkW5tP+dKE#!_})41(^AOqv@rRP5|#b zDOQwRLhk<6SO{^1G!eeXf`GcLwYO3W{8(m@wfXM&1XS9Z`+>Q zi`B;X4E+foa+`sP+wLIR7#9*Wz@}gXJF%wNAQF9S2m<0|w9UX1<5>t5&T#7@%oTq| z+hq69#t5MkKUQg`aB*&s`;IzUZX6>i&pnCDwtZ?jM0AdZ!IxTe7raXPb7;%wjJ7$e9$#ub zZ<017a|?dAH}b!y7BkktvPR**&MKDVE0WYLl~abvU&g9Ss(=hrsL;4}se&?0;mYaS zq)Nyz$rGyx5QWa#F(n5@Ksfsd8B_T$clmZ#1vJg{QN|6ksM2IOZrBH@Z zCd3q$QY^zL6=HHrX^>%*3o)5enq(L)LQJBRMj1w%5EGNqEW>COVjiR@fNq(!!BWfD g_bn|gEiD!K51N{hSfy9ULjV8(07*qoM6N<$f+dSdasU7T literal 0 HcmV?d00001 diff --git a/images/tab-listen-new.png b/images/tab-listen-new.png new file mode 100644 index 0000000000000000000000000000000000000000..df35d1a07491844b6e0c36219bbaf398b370b6c9 GIT binary patch literal 1382 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 Z{{n4Xj5wb!`1=3=002ovPDHLkV1m@q|F-}D literal 0 HcmV?d00001 diff --git a/images/tab-message-active-nodot.png b/images/tab-message-active-nodot.png new file mode 100644 index 0000000000000000000000000000000000000000..fe4c0722bdf82cd30b0f490bbdb03eb93ee170e7 GIT binary patch literal 1514 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 jEY)Bz7z_r3L5}|eg8^Xh5mlK~#7F?OR=P z5=#_5Ju@Nd6Rtp9fpG=m3XG46(XayJgXX601LVb2^dUDZ01b(c;tIqa;0olfKwi`f z%+LMK6r>V3J<~HV{8aH(rIK_{H_bPF{=13RSYwSf)|eR-EeN%vTEQWwVA)n-)aD0% zmwf74zSS-LU24+;Fc(5<)mlDxE4S_Y{+4A~574w_IFw0ggQzdA@3zYaV%fE#LD$JU@6|r;IRjLc-l}GxriR@8RD(Wr2V?p6?xILCj1E`FQkk z*S72mW-ZV>kO46>A%qp=hP_dnm1radKDX8LdgWQ@n5hXldwaHRIaZBK#Y0gSZ}vMB zY#aKeqXO0SEm#)-)+HaPARl-^0m^p^&)&ncX7kQyw@gLI`TO$<7E+CU5G1fNB%7M< zFIbCexl4vPdk@9zt6fW351>>Fu>-K4(thcXQX(}WCS+hMcYNia4t@>lNl4Is=DYJ< z%vp&gW(rbL5fV!Vi-k|8SX7lVK;yWvhaK}Wl9-gFBqqdQXMD3fYxl{)lMZEwPe-2$ zffLk%ASh}dNxV&?RhfqLiza5U1}HHC@eF1Zr8M{3=}I2dU4#WpC@oX$gVu zC`#?fUwokz0U~X2CnKGpO>%1qL89iK)ZY8kYtf3pbzIKcPweJl=3j)C5bT6W76baW z3gR*Xiwz8eRNpD+#yCjI6q&pfD`DTO3u#xKqOkpat#0Qn8w?N&I=XS z%vD3F-8kB1fRK)^j~N#eM~bTaHj|DC`x1;%juK-P5GL+wc;KSxO+-i(kRH&QveDKa zhhsUrlyJVNF;V7p9IkS+B_s;08>DtPelu0jR$xtCDV!N0L(foz<2zzsVCi*|?c1_9 zY*8!m7y-&t9eH{IZs3Y<4yL$38nB6j6?!$%fdrB+D&BdGAWO(}@jSB{gP5j32_O=3 z!@D680xNKbBJgbg*+0mU+=cRe#Xs;AAI2q+IMa1?4FApZBpHL)>b7nS)+~yVOKwMM z*M|Qn3jL(X@?;xXsr$4-XuNNH#m+8{x8r+=-(Byi${ya9Ng8gMAd$fyx9)j;EE5FZ zrFNv@Oi%wT99$Qc+M!pp%3Mo`E>eVt_<~jhTqKv;ZDqG~W3+_mB1K?(6~rb?a49jt z6&bN>2?@<81(K{8Wv>?v`@hg@oeWU@;7e4*9_g6Dq{*htAbY)N=&A)U!}X|fk%WX! zkfz3=3%-^C;RywymK0VC_Mb+2v*}&;9xY{}NE<g2i%rI5o#B0QlWU|374 zQw*Z(GNiX;Xc3POG3N~4w#t+*^%U53R3VTRT(@|0IbEw@94&=VqyOE=DJ%?raLUY{W* z`q&D&n5e?G1d)*8X&XsgjK~OG`%TJcah=2XazVTkG$5jji6jXJMY%!lr|@hSn))`4FLI0o(xV<;1p4E)OqDXv@7#2X zEWSgX78_J@DlsnO7KMo&5(Y)K_-ylXk;sV*F9wAqD6`X@d|rJQ1ada6&4=}U^1pND z{c7!mbD;SD&Twwy+zVTiP=UTlrZXnz8AIb;qbZ#W@jd?#B-}B5Q}YDY)a?E4m1p4& zTq7hxT$U1-yW;h9yW%RYiA;pJB!i+Z--9u_&XH<*)nQlMBsNKkYlK)1(t`n|!<9QR zqc=9X>w?(lk?}>Ki1|6FL~*M=%N;TKF+-#hmjsIp6TTj88{d?=a`P(9#iO*iB!sV9 z__qJUV8kT}(=2jmvI|INUn2(fSw7U)Sya4qO{wb6c3!?M3Is{}qhQj(B xk}@O#&Gr7T{z{xY%nTejty+a{jWyj(Q literal 0 HcmV?d00001 diff --git a/images/tab-message.png b/images/tab-message.png new file mode 100644 index 0000000000000000000000000000000000000000..08c90175e6311b2e20c8b7778bcc2df013dc5c4b GIT binary patch literal 2482 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 literal 0 HcmV?d00001 diff --git a/images/tab-profile-active.png b/images/tab-profile-active.png new file mode 100644 index 0000000000000000000000000000000000000000..1f81690e44cb5f758e3f60d05330db167f5c05da GIT binary patch literal 2136 zcmV-e2&eanP)d3L(5|92x7}P~4jYcl%6n(_9FJ#aZ+o(%6(mkT2@(fxp4=e`A0Z(~;9TIq zVLYCI#EHoj$zYCw1H&a;5Lhx&$hf;6uf`7I%Uv~2bysy#KPjV$`Z@0Ie^k4C5N3UO{b>c*wH9MPSR@71t-?qOS{R7kKLp%TJT<&Z^AcQrQc@afJb@pXF2#8>W>7TuM z$&V;|urKQ`-@F8Y5X4m0U+ymwiq&a;hSxG1{KJU1U*CfxRfOYb%;WbluW^27jt~pVGXJn8^ZioP2FGB-kdv7hyU<{scA0Bj;uQ za>Qg=Low@f0qS?e@My6yBUBgQfkn}X4Motku^eZq*R_pTVK}+iHrnVZh~y9)fq=J^ zqW6t}@E547Yi>zGpJy(b@q|coT-Fv3jlnJA9tP22d0NZt~}Lo(ZCS8a(08#}L%>54&*o zIq<0A=`6o4gOva96o?mLdZ&3&X+(VnMT2sf%}=!RICvmjVIFb32$NA}PIXXr6pF?ovH2L*egF^n88MH6F>{M3 z{s~2+=t+}FD(`y9mw6Fpl2XTfaX4Hy-n0is;GDr?%UAl}r;pjpJIK|{xxHdnAt0tl zIkkQ~*NF1Zu}J)_{7J1DZ4l?7C`dkIGC>K(b1VvP!YHNs7G}*NMvWdVJe4+0b_t0` zPngcN&WLZ$&}7GH`ju4I@m)bXM~K8*W;@(`WD8|{(^oBwjwT3J1vRVRiOGsh{U2*^ z89d^DGsf~@#E#yWqT%$(FjLt@DUySwYIvleNOW1*JT;OSwXdFd0zBe<?>P4JNN`>s?FmY1nT|Vk~5DG7UMWiThD@} zAQKd=wy!Om1kWT808~p)?ro2?O|j1*2)^0Gl}PxKMZHaX3CTVG!RegjI3JrXXy7TV zChazB6iqOj$_LL6p2`$QX{LPu1tKNXo3A&&fB*<4yR2^rDm)LK;8OQeD57YBO8d92 z&fgFjwM}_6x8Rl*rFErs0|Fw*SN$gYnWytP2ZMczOjJ9hk#e6P3uTqnnNL9Dc*LXH2-@NRe79quFWUg|hyc!HPm0R$mki%?iGj-gAR46ilHbMvFoa z#&#grkeC$Px=;{>3G*TtNKqG*WpxeIg0 z!Lhleqzz7lp+q#yMtBn{jzBVBgOX0V0=f|f$lTwu=vIm)kg3TvEkci-@Y&7 zX?7zFPTE;D_B)(N{Ph3tw8AYTPeJeW;A5U=UfSP6IO)~wCRdi%^M%A{;k6Sst$Zs% z=UQ{f`LcbtDv`{b{I9c4glS!w*Bi#We>k8!<_WjkdV-*qHvUW;&Z1v%wNu9F_9b0n z7HQG~1v5&(t*Z2GIVip5kJ^A)o$Jn9P}^CJZBZhTNF)-8L?V&Mqs-6PjmvvIk3O>i O0000K~#7F?VVjt z8%Yqyt7rUy$qP9_oD;-30nP~miWFgtgE>J!OZbo?F+8kTAgd%NzyyP%yo8)!hZC%G zg7uyt-j@Z;%=A{xnpLC)cMs`tx1oO$31Z4%`|s+muCA^Duvjb>i^XEGSS*&&g29Ng z=j}}t#tIJt7L*uc6ZlFd;tL$Q__Gec2nJJ-gU-R$fd__SoX60lp3P_9F_+E55a67@n_Hb*0>dy$ zm{^oT*2gI85e%1h@9*A|1#h7XhG5iCrb!f{lpoK327@q0nC5o#ttL@oFr+Tia{xmy zLYSlNqj?l%G09^n^u>R?D2$%;!rsH`N|n(U4oUavXK1pB^GBkni=M;5&cRDC0PS;3 zKP2|?jea0wPlAMDxLI4T`7-txd6k*N8u+G5{eGm2;y%i>=+(M#Lje;vo~cu^bI)3!o9L9z))eb8~-)>~Q?=@{mu(4IP-Lbronrn=qJic`6)IICN^OH4hYXo;%-^h}b6FaaUXTu1%Q0354FlUG6~& zEG#W_Gb|%qv$&Bv4Vut5lrbm?EV^^cbNi4Ir`gke3MvK~F_;8RXcGp<`I6we4=F-k zgRlO0ppp2)lNwrtX>GNNBAd_+eMlLM(ApxKc-K?|T7<#5L-=AH?sOn+BIt-r+`!cm znHjrL)(d)nLCRnzL6~+q<)`;S1I7w-*U=&j3z;z0k0WHFu zjhzXH5Eihf6qO(RdhiHyXuecwQA*&tsWh`T^A!xhnCx=xr`lKi^d~BlRS9}r-4sy> zqtgDftF!y~shtw;#VdsV7Kt*uJeyj5!A&sosNYTJoW$Uq;&OM&Bu}}-$c?gv^@Yww zbWz4{8;}+eyzia%9-Al%Vf?A(!=DdJJjdTA38x2<^t6C3M&_J99j92f)bU`i|$<7$DNEv66vH`SKp>@lpSbY&FcxdCbP8fw>g7ctl()mh@#bU8o zEEbE!V!4eNv%sWtC_KakoZMQZ;C)8SMnhG2?(iSJRp^QX{ zm{c38(S-r^x$z(cQW1uRuUqLUSAgO5(%h>#<*!w_FmX6c{gM-e!nEs%YljAeaB|Mh z+flfM;O)?*L$DFa!vt_l_o60-<#cdR2Md8q)0`4?>#>nt| zohvk*#Jc-^B7rg ziA7m8Ta@pqysB(E%<&{li><9z85E&R7@RcgA{#2U8zoULA#w0O;c6e>0}m9TM40}W z`Lf6^-^$9`6|!eIYdQ=ei^fS!Y?kk}%iSE;R9ZH0uc2-yJi}yR`!7anhxaBLwVX#RRHlP5d1%Le;jy|Om zVNnpBj7r~@n{u>$M7`BY)FS0PcixC^uVt}VEEbE!VzF2(ca(ns9Mtcu3`eiM00000 LNkvXXu0mjfWe3#) literal 0 HcmV?d00001 diff --git a/images/tab-service-active.png b/images/tab-service-active.png new file mode 100644 index 0000000000000000000000000000000000000000..ad2af3ca2503bfa417ee59dd04d59ad1fabdd0d2 GIT binary patch literal 2894 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 literal 0 HcmV?d00001 diff --git a/images/tab-service.png b/images/tab-service.png new file mode 100644 index 0000000000000000000000000000000000000000..b5eebbca53f2dab3e98b64365d37935ebd2fcaa0 GIT binary patch literal 2615 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 literal 0 HcmV?d00001 diff --git a/images/tab-user.png b/images/tab-user.png new file mode 100644 index 0000000000000000000000000000000000000000..da5f6fcb820754b9194b74b44a56ceb1c023daf9 GIT binary patch literal 1054 zcmV+(1mXLMP)F&zv2J(o#s`yP30eQQEW-w2%%Y3P9-~Bs|iPCJAU_2NHX2$FaG`dOv9;+jpKD z|2#K4J9|o5EEbE!VzF2(7K?dIr>1u@@?{3TO+XK1$ZPpbj;VHqkS-8Vo`AkF@^9Uc zV{(o=kk_Tpsz#Dgw;LC8jxHMJ2B1FVLe}M+dIYIpHv|7Dx96q3ODd@l(k+cZ6Wg`N$deWJ9KNBF z3L!nx++NOh?J;sjvm6;vNriw0G!c|(!d_Nm@1aU6KKM87Ef!*uCZwN6CFvjtc2ucf zdK$IGzaqq!TE!Ps2@nq*0@~i1mrWZ@4FRn-T?73~-m79$ zY|AxN((uyWaYoL`dZ|^S)G7fOIgmA*N>H11DFk84qv}_zyNgrLr}mWUnoxzQ5DLi; zvcWzTC&p4^dl~sv-CIF&5fYUar0E97aOt}{`>)JsoI3T+uH6kk7oK(mZPLz^kxnoOVZs?UZ zC@Urg@h#GtiHoc9ipUX%L-z&wdX-3ep`yS8uTxv+07E1WRo{7CE@TR4LU)FcQW|mQ zMquQiiU`R`Ym-8yNKZm{hLLA90`;qiSzEJJ)<&_#ROSyl=~-L%EUhR(`q7rj);K=H3AK)m=L*t9JK>+_m87?AnyKg z)DFbmKh{0W1l*@$LgfAh*`R}gXE$7YHcn4l#KONooIviC5hrny;-AH0u~;k?i$x`y YKlIpCcNpREy8r+H07*qoM6N<$f)rWfRR910 literal 0 HcmV?d00001 diff --git a/images/wenyu-type-02.png b/images/wenyu-type-02.png new file mode 100644 index 0000000000000000000000000000000000000000..bd088f839a2b6f5b4d70ddbf331af88a6030cfc4 GIT binary patch literal 8699 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 + + + + + 返回 + + 文章详情 + + + + + + + {{article.title}} + + {{article.date}} + {{article.author || '心伴康养'}} + + + + + + + + + + + 阅读 {{article.views}} + + + + diff --git a/pages/academy/detail/detail.wxss b/pages/academy/detail/detail.wxss new file mode 100644 index 0000000..eb4523a --- /dev/null +++ b/pages/academy/detail/detail.wxss @@ -0,0 +1,127 @@ +.page { + min-height: 100vh; + background-color: #fff; + display: flex; + flex-direction: column; +} + +.unified-header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 194rpx; + background: linear-gradient(135deg, #B06AB3 0%, #9B59B6 100%); + border-bottom: none; + display: flex; + align-items: flex-end; + justify-content: space-between; + padding: 0 32rpx 20rpx; + z-index: 1000; +} + +.unified-header-left { + display: flex; + align-items: center; + gap: 8rpx; + width: 160rpx; + height: 56rpx; +} + +.unified-back-icon { + width: 56rpx; + height: 56rpx; + color: #ffffff; +} + +.unified-back-text { + font-size: 34rpx; + font-weight: bold; + color: #ffffff; +} + +.unified-header-title { + font-size: 40rpx; + font-weight: bold; + color: #ffffff; + flex: 1; + text-align: center; +} + +.unified-header-right { + width: 160rpx; + height: 56rpx; +} + +.content { + flex: 1; +} + +.article-content { + padding: 30rpx; +} + +.header { + margin-bottom: 30rpx; +} + +.title { + font-size: 36rpx; + font-weight: bold; + color: #333; + line-height: 1.4; + margin-bottom: 20rpx; +} + +.meta { + font-size: 24rpx; + color: #999; +} + +.meta text { + margin-right: 20rpx; +} + +.author { + font-size: 30rpx; + color: #576b95; +} + +.cover { + width: 100%; + border-radius: 12rpx; + margin-bottom: 30rpx; +} + +.body { + font-size: 36rpx; + color: #444; + line-height: 1.6; +} + +.body h3 { + font-size: 42rpx; + font-weight: bold; + color: #333; + margin: 30rpx 0 15rpx; + display: block; +} + +.body p { + margin-bottom: 20rpx; + display: block; +} + +.date{ + font-size: 30rpx; +} + +.footer { + margin-top: 50rpx; + padding-bottom: 50rpx; +} + +.read-count { + font-size: 36rpx; + color: #999; +} diff --git a/pages/academy/list/list.js b/pages/academy/list/list.js new file mode 100644 index 0000000..744aa39 --- /dev/null +++ b/pages/academy/list/list.js @@ -0,0 +1,156 @@ +const api = require('../../../utils/api') + +Page({ + data: { + articles: [], + categories: [], + activeCategory: null, + page: 1, + hasMore: true, + loading: false, + error: null + }, + + onLoad() { + this.loadCategories() + this.loadArticles() + }, + + onBack() { + wx.navigateBack() + }, + + onArticleTap(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/academy/detail/detail?id=${id}` + }) + }, + + async loadCategories() { + try { + const res = await api.happySchool.getCategories() + console.log('[happy-school] 分类响应:', res) + + if (res.success && res.data) { + let categories = [] + + if (res.data.length > 0 && res.data[0].id !== null) { + categories = [ + { id: null, name: '全部' }, + ...res.data.map(cat => ({ + id: cat.id, + name: cat.name + })) + ] + } else { + categories = res.data.map(cat => ({ + id: cat.id, + name: cat.name + })) + } + + this.setData({ categories }) + } + } catch (err) { + console.error('[happy-school] 加载分类失败:', err) + } + }, + + async loadArticles(reset = true) { + if (this.data.loading) return + if (!reset && !this.data.hasMore) return + + this.setData({ loading: true, error: null }) + const page = reset ? 1 : this.data.page + 1 + + try { + const params = { + page, + limit: 20 + } + + if (this.data.activeCategory) { + params.categoryId = this.data.activeCategory + } + + console.log('[happy-school] 请求文章列表:', params) + const res = await api.happySchool.getArticles(params) + console.log('[happy-school] 文章响应:', res) + + if (res.success && res.data) { + let list = [] + + if (Array.isArray(res.data)) { + list = res.data + } else if (res.data.data && Array.isArray(res.data.data)) { + list = res.data.data + } else if (res.data.list && Array.isArray(res.data.list)) { + list = res.data.list + } + + const articles = list.map(item => ({ + id: item.id, + title: item.title, + summary: item.summary || '', + cover: this.processImageUrl(item.coverImage), + date: this.formatDate(item.publishTime), + views: item.readCount || 0, + categoryName: item.categoryName || '' + })) + + this.setData({ + articles: reset ? articles : [...this.data.articles, ...articles], + page, + hasMore: articles.length >= params.limit, + loading: false + }) + } else { + this.setData({ + loading: false, + error: res.error || '加载失败' + }) + } + } catch (err) { + console.error('[happy-school] 加载文章失败:', err) + this.setData({ + loading: false, + error: err.message || '加载失败' + }) + } + }, + + onCategoryTap(e) { + const categoryId = e.currentTarget.dataset.id + if (categoryId === this.data.activeCategory) return + + this.setData({ + activeCategory: categoryId, + page: 1, + hasMore: true, + articles: [] + }) + this.loadArticles(true) + }, + + processImageUrl(url) { + if (!url) return '' + if (url.startsWith('http://') || url.startsWith('https://')) { + return url + } + return 'https://ai-c.maimanji.com' + (url.startsWith('/') ? '' : '/') + url + }, + + formatDate(dateStr) { + if (!dateStr) return '' + const date = new Date(dateStr) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` + }, + + onReachBottom() { + this.loadArticles(false) + } +}) diff --git a/pages/academy/list/list.json b/pages/academy/list/list.json new file mode 100644 index 0000000..c8c2357 --- /dev/null +++ b/pages/academy/list/list.json @@ -0,0 +1,6 @@ +{ + "usingComponents": { + "app-icon": "/components/icon/icon" + }, + "navigationBarTitleText": "心伴学堂" +} \ No newline at end of file diff --git a/pages/academy/list/list.wxml b/pages/academy/list/list.wxml new file mode 100644 index 0000000..6eba3ee --- /dev/null +++ b/pages/academy/list/list.wxml @@ -0,0 +1,56 @@ + + + + + + 返回 + + 心伴学堂 + + + + + + + + + + {{item.name}} + + + + + + + + + + + + + + 加载中... + + + + + 暂无内容 + + + + {{error}} + 点击重试 + + + diff --git a/pages/academy/list/list.wxss b/pages/academy/list/list.wxss new file mode 100644 index 0000000..835befe --- /dev/null +++ b/pages/academy/list/list.wxss @@ -0,0 +1,180 @@ +.page { + min-height: 100vh; + background-color: #f8f8f8; + display: flex; + flex-direction: column; +} + +.unified-header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 194rpx; + background: linear-gradient(135deg, #B06AB3 0%, #9B59B6 100%); + border-bottom: none; + display: flex; + align-items: flex-end; + justify-content: space-between; + padding: 0 32rpx 20rpx; + z-index: 1000; +} + +.unified-header-left { + display: flex; + align-items: center; + gap: 8rpx; + width: 160rpx; + height: 56rpx; +} + +.unified-back-icon { + width: 56rpx; + height: 56rpx; +} + +.unified-back-text { + font-size: 34rpx; + font-weight: bold; + color: #ffffff; +} + +.unified-header-title { + font-size: 40rpx; + font-weight: bold; + color: #ffffff; + flex: 1; + text-align: center; +} + +.unified-header-right { + width: 160rpx; + height: 56rpx; +} + +.content { + flex: 1; +} + +.category-bar { + background: #fff; + padding: 20rpx 0; + border-bottom: 1rpx solid #eee; +} + +.category-scroll { + white-space: nowrap; +} + +.category-list { + display: inline-flex; + padding: 0 30rpx; +} + +.category-item { + display: inline-block; + padding: 16rpx 32rpx; + font-size: 30rpx; + color: #666; + background: #f5f5f5; + border-radius: 36rpx; + margin-right: 20rpx; + flex-shrink: 0; +} + +.category-item.active { + background: linear-gradient(135deg, #B06AB3 0%, #9B59B6 100%); + color: #fff; +} + +.article-list { + padding: 20rpx 30rpx; +} + +.article-item { + display: flex; + background-color: #fff; + border-radius: 20rpx; + padding: 20rpx; + margin-bottom: 20rpx; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); +} + +.article-cover { + width: 200rpx; + height: 150rpx; + border-radius: 12rpx; + margin-right: 20rpx; + flex-shrink: 0; + background-color: #eee; +} + +.article-info { + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.article-title { + font-size: 38rpx; + font-weight: bold; + color: #333; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + overflow: hidden; +} + +.article-desc { + font-size: 30rpx; + color: #666; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + margin: 10rpx 0; +} + +.article-footer { + display: flex; + justify-content: space-between; + align-items: center; +} + +.article-date, .article-views { + font-size: 28rpx; + color: #999; +} + +.empty-tip, .loading-tip, .error-tip { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: 200rpx; +} + +.empty-tip image { + width: 240rpx; + height: 240rpx; + margin-bottom: 20rpx; +} + +.empty-tip text, .loading-tip text, .error-tip text { + font-size: 28rpx; + color: #999; +} + +.error-tip text:first-child { + color: #ff6b6b; + margin-bottom: 20rpx; +} + +.retry-btn { + background: linear-gradient(135deg, #B06AB3 0%, #9B59B6 100%); + color: #fff !important; + padding: 16rpx 48rpx; + border-radius: 36rpx; + font-size: 28rpx !important; +} diff --git a/pages/activity-detail/activity-detail.js b/pages/activity-detail/activity-detail.js new file mode 100644 index 0000000..d4a6614 --- /dev/null +++ b/pages/activity-detail/activity-detail.js @@ -0,0 +1,767 @@ +// pages/activity-detail/activity-detail.js - 活动详情页面 +const api = require('../../utils/api') +const util = require('../../utils/util') +const imageUrl = require('../../utils/imageUrl') +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + loading: false, + + // 活动ID + activityId: '', + + // 活动详情 + activity: { + id: '', + title: '', + cover_image: '', + description: '', + start_date: '', + start_time: '', + end_date: '', + end_time: '', + address: '', + venue: '', + province: '', + city: '', + district: '', + organizer: '', + contact_phone: '', + price: 0, + is_free: true, + price_text: '', + participants_count: 0, + max_participants: 0, + status: 'upcoming', // upcoming/ongoing/ended/full + is_favorited: false, + images: [] + }, + + // 状态文字 + statusText: '即将开始', + signupButtonText: '立即报名', + + // 参与者列表(显示前6个) + participants: [], + + // 所有参与者(用于弹窗) + allParticipants: [], + + // 显示参与者弹窗 + showParticipantsModal: false, + + // 二维码引导弹窗 + showQrcodeModal: false, + qrcodeImageUrl: '', + + // 推荐活动列表 + recommendList: [] + }, + + onLoad(options) { + // 计算导航栏高度 + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight + }) + + // 获取活动ID + if (options.id) { + this.setData({ activityId: options.id }) + this.loadActivityDetail() + this.loadParticipants() + this.loadRecommendList() + } else { + wx.showToast({ + title: '活动不存在', + icon: 'none' + }) + setTimeout(() => { + wx.navigateBack() + }, 1500) + } + }, + + /** + * 提取HTML中的所有图片URL + */ + extractImageUrls(html) { + if (!html) return [] + + const urls = [] + const imgRegex = /]+src="([^"]+)"[^>]*>/gi + let match + + while ((match = imgRegex.exec(html)) !== null) { + urls.push(match[1]) + } + + return urls + }, + + /** + * 处理HTML中的图片样式 + */ + processHtmlImages(html) { + if (!html) return '' + + let imageIndex = 0 + + // 给所有img标签添加样式,确保图片不超出屏幕宽度 + // 注意:rich-text中需要使用px单位,不能使用rpx + return html.replace( + /]*?)(?:\s+style="[^"]*")?([^>]*)>/gi, + (match, before, after) => { + // 移除原有的style属性,添加新的样式和data-index + const cleanBefore = before.replace(/\s+style="[^"]*"/gi, '') + const cleanAfter = after.replace(/\s+style="[^"]*"/gi, '') + const result = `` + imageIndex++ + return result + } + ) + }, + + /** + * 返回上一页 + */ + onBack() { + wx.navigateBack() + }, + + /** + * 分享 + */ + onShare() { + wx.showShareMenu({ + withShareTicket: true, + menus: ['shareAppMessage', 'shareTimeline'] + }) + }, + + /** + * 分享给好友 + */ + onShareAppMessage() { + const { activity } = this.data + const referralCode = wx.getStorageSync('referralCode') || '' + const referralCodeParam = referralCode ? `&referralCode=${referralCode}` : '' + return { + title: activity.title, + path: `/pages/activity-detail/activity-detail?id=${activity.id}${referralCodeParam}`, + imageUrl: activity.cover_image + } + }, + + /** + * 分享到朋友圈 + */ + onShareTimeline() { + const { activity } = this.data + const referralCode = wx.getStorageSync('referralCode') || '' + const query = `id=${activity.id}${referralCode ? `&referralCode=${referralCode}` : ''}` + return { + title: activity.title, + query: query, + imageUrl: activity.cover_image + } + }, + + /** + * 加载活动详情 + */ + async loadActivityDetail() { + this.setData({ loading: true }) + + try { + const res = await api.activity.getDetail(this.data.activityId) + + if (res.success && res.data) { + const activity = { + ...res.data, + is_favorited: res.data.isLiked || res.data.is_favorited // 兼容后端字段 + } + + console.log('[activity-detail] 活动详情数据:', activity) + + // 处理图片数组 + let images = [] + if (activity.images) { + images = typeof activity.images === 'string' + ? JSON.parse(activity.images) + : activity.images + } + + // 格式化日期时间(兼容多种字段名) + const startDate = this.formatDate( + activity.startDate || activity.start_date || activity.activityDate + ) + const startTime = this.formatTime(activity.startTime || activity.start_time || '') + const endDate = activity.endDate || activity.end_date || activity.activityDate || '' + const endTime = activity.endTime || activity.end_time || '' + + // 处理封面图片URL - 后端已返回完整URL,前端只需兜底处理 + const coverImage = imageUrl.getActivityCoverUrl(activity.coverImage || activity.cover_image) + + // 处理地址信息 + const province = activity.province || '' + const city = activity.city || '' + const district = activity.district || '' + const venue = activity.venue || '' + const address = activity.address || `${province}${city}${district}${venue}` + + // 处理主办方信息(兼容对象和字符串格式) + let organizer = '主办方' + if (activity.organizer) { + if (typeof activity.organizer === 'object' && activity.organizer.nickname) { + organizer = activity.organizer.nickname + } else if (typeof activity.organizer === 'string') { + organizer = activity.organizer + } + } + + // 处理活动详情内容 + let description = activity.description || '暂无活动详情' + let detailHtml = '' + let detailImageUrls = [] + + // 如果有detailContent(HTML格式),使用rich-text渲染 + if (activity.detailContent) { + // 提取HTML中的所有图片URL + detailImageUrls = this.extractImageUrls(activity.detailContent) + // 处理HTML,添加图片样式和点击事件标记 + detailHtml = this.processHtmlImages(activity.detailContent) + } else if (activity.description) { + description = activity.description + } + + // 判断活动状态(兼容多种字段名) + const status = this.getActivityStatus({ + ...activity, + start_date: activity.startDate || activity.start_date || activity.activityDate, + start_time: activity.startTime || activity.start_time || '00:00', + end_date: activity.endDate || activity.end_date || activity.activityDate, + end_time: activity.endTime || activity.end_time || '23:59', + max_participants: activity.maxParticipants || activity.max_participants || 0, + participants_count: activity.currentParticipants || activity.participants_count || 0 + }) + const statusText = this.getStatusText(status) + const signupButtonText = this.getSignupButtonText(status) + + this.setData({ + activity: { + id: activity.id, + title: activity.title || '活动标题', + cover_image: coverImage, + description: description, + detailHtml: detailHtml, + detailImageUrls: detailImageUrls, + start_date: startDate, + start_time: startTime, + end_date: endDate, + end_time: endTime, + address: address, + venue: venue, + province: province, + city: city, + district: district, + organizer: organizer, + contact_phone: activity.contactInfo || activity.contact_phone || '', + price: activity.price || 0, + is_free: activity.isFree !== undefined ? activity.isFree : activity.is_free, + price_text: activity.priceText || activity.price_text || '免费', + participants_count: activity.currentParticipants || activity.participants_count || 0, + max_participants: activity.maxParticipants || activity.max_participants || 0, + is_favorited: activity.isLiked || activity.is_favorited || false, + images: images, + status: status, + activity_guide_qrcode: activity.activity_guide_qrcode || activity.activityGuideQrcode || '' + }, + statusText, + signupButtonText, + qrcodeImageUrl: activity.activity_guide_qrcode || activity.activityGuideQrcode || '' + }) + + console.log('[activity-detail] 封面图片URL:', coverImage) + console.log('[activity-detail] 活动地址:', address) + console.log('[activity-detail] 主办方:', organizer) + console.log('[activity-detail] 详情HTML:', detailHtml ? '有HTML内容' : '无HTML内容') + } + } catch (err) { + console.error('加载活动详情失败', err) + wx.showToast({ + title: '加载失败', + icon: 'none' + }) + } finally { + this.setData({ loading: false }) + } + }, + + /** + * 加载参与者列表 + */ + async loadParticipants() { + try { + const res = await api.activity.getParticipants(this.data.activityId, { + page: 1, + limit: 50 + }) + + if (res.success && res.data) { + const allParticipants = res.data.list.map(item => ({ + id: item.id, + name: item.real_name || item.nickname || '匿名用户', + avatar: item.avatar || '/images/default-avatar.png', + join_time: this.formatJoinTime(item.created_at) + })) + + // 前6个用于列表显示 + const participants = allParticipants.slice(0, 6) + + this.setData({ + participants, + allParticipants + }) + } + } catch (err) { + console.log('加载参与者列表失败', err) + } + }, + + /** + * 加载推荐活动 + */ + async loadRecommendList() { + try { + const res = await api.activity.getList({ + limit: 10, // 获取10个,前端只显示8个 + sortBy: 'date' + }) + + console.log('[activity-detail] 推荐活动API响应:', res) + + if (res.success && res.data && res.data.list) { + const recommendList = res.data.list + .filter(item => item.id !== this.data.activityId) // 排除当前活动 + .slice(0, 8) // 只取前8个 + .map(item => { + // 处理封面图片URL - 后端已返回完整URL,前端只需兜底处理 + const coverImage = imageUrl.getActivityCoverUrl(item.coverImage || item.cover_image) + + // 处理日期字段(兼容多种命名) + const startDate = item.startDate || item.start_date || item.activityDate || '' + + console.log('[activity-detail] 推荐活动:', item.title, '封面:', coverImage, '日期:', startDate) + + return { + id: item.id, + title: item.title || '活动标题', + cover_image: coverImage, + start_date: this.formatDate(startDate) + } + }) + + console.log('[activity-detail] 推荐活动列表:', recommendList) + this.setData({ recommendList }) + } + } catch (err) { + console.error('加载推荐活动失败', err) + } + }, + + /** + * 判断活动状态 + */ + getActivityStatus(activity) { + const now = new Date() + const startDate = new Date(activity.start_date + ' ' + activity.start_time) + const endDate = new Date(activity.end_date + ' ' + activity.end_time) + + // 已满员 + if (activity.max_participants > 0 && + activity.participants_count >= activity.max_participants) { + return 'full' + } + + // 已结束 + if (now > endDate) { + return 'ended' + } + + // 进行中 + if (now >= startDate && now <= endDate) { + return 'ongoing' + } + + // 即将开始 + return 'upcoming' + }, + + /** + * 获取状态文字 + */ + getStatusText(status) { + const statusMap = { + upcoming: '即将开始', + ongoing: '进行中', + ended: '已结束', + full: '已满员' + } + return statusMap[status] || '即将开始' + }, + + /** + * 获取报名按钮文字 + */ + getSignupButtonText(status) { + const buttonTextMap = { + upcoming: '立即报名', + ongoing: '立即报名', + ended: '活动已结束', + full: '已满员' + } + return buttonTextMap[status] || '立即报名' + }, + + /** + * 格式化日期 + */ + formatDate(dateStr) { + if (!dateStr) return '' + const date = new Date(dateStr) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}年${month}月${day}日` + }, + + /** + * 格式化时间 + */ + formatTime(timeStr) { + if (!timeStr) return '' + return timeStr.substring(0, 5) // HH:mm + }, + + /** + * 格式化加入时间 + */ + formatJoinTime(dateStr) { + if (!dateStr) return '' + return util.formatTime(dateStr, 'MM-DD HH:mm') + }, + + /** + * 拨打电话 + */ + onCallPhone() { + const phone = this.data.activity.contact_phone + if (!phone) return + + wx.makePhoneCall({ + phoneNumber: phone + }) + }, + + /** + * 预览图片 + */ + onPreviewImage(e) { + const url = e.currentTarget.dataset.url + const urls = this.data.activity.images + + wx.previewImage({ + current: url, + urls: urls + }) + }, + + /** + * 点击详情区域的图片(rich-text中的图片) + */ + onDetailImageTap(e) { + // 获取点击位置 + const { clientX, clientY } = e.detail || e.touches[0] || {} + + // 通过createSelectorQuery获取所有图片的位置信息 + const query = wx.createSelectorQuery() + query.selectAll('.detail-rich-text-wrapper >>> img').boundingClientRect() + query.exec((res) => { + if (!res || !res[0] || res[0].length === 0) { + console.log('[activity-detail] 未找到图片元素') + return + } + + const imageRects = res[0] + const detailImageUrls = this.data.activity.detailImageUrls || [] + + // 判断点击位置是否在某个图片上 + for (let i = 0; i < imageRects.length; i++) { + const rect = imageRects[i] + if (clientX >= rect.left && clientX <= rect.right && + clientY >= rect.top && clientY <= rect.bottom) { + // 点击了第i张图片 + console.log('[activity-detail] 点击了第', i, '张图片') + + if (detailImageUrls.length > 0) { + wx.previewImage({ + current: detailImageUrls[i], + urls: detailImageUrls + }) + } + break + } + } + }) + }, + + /** + * 查看全部参与者 + */ + onViewAllParticipants() { + this.setData({ showParticipantsModal: true }) + }, + + /** + * 关闭参与者弹窗 + */ + onCloseParticipantsModal() { + this.setData({ showParticipantsModal: false }) + }, + + /** + * 关闭二维码弹窗 + */ + onCloseQrcodeModal() { + this.setData({ showQrcodeModal: false }) + }, + + /** + * 保存二维码 + */ + async onSaveQrcode() { + try { + const qrcodeUrl = this.data.qrcodeImageUrl || this.data.activity.activity_guide_qrcode + if (!qrcodeUrl) { + wx.showToast({ title: '二维码不存在', icon: 'none' }) + return + } + + wx.showLoading({ title: '正在保存...' }) + + const downloadRes = await new Promise((resolve, reject) => { + wx.downloadFile({ + url: qrcodeUrl, + success: resolve, + fail: reject + }) + }) + + if (downloadRes.statusCode !== 200) throw new Error('下载失败') + + await new Promise((resolve, reject) => { + wx.saveImageToPhotosAlbum({ + filePath: downloadRes.tempFilePath, + success: resolve, + fail: reject + }) + }) + + wx.hideLoading() + wx.showToast({ title: '保存成功', icon: 'success' }) + this.onCloseQrcodeModal() + } catch (err) { + wx.hideLoading() + console.error('保存失败', err) + wx.showToast({ title: '保存失败', icon: 'none' }) + } + }, + + /** + * 阻止冒泡 + */ + preventBubble() { + return + }, + + /** + * 点击推荐活动 + */ + onRecommendTap(e) { + const id = e.currentTarget.dataset.id + wx.redirectTo({ + url: `/pages/activity-detail/activity-detail?id=${id}` + }) + }, + + /** + * 收藏/取消收藏 + */ + async onToggleFavorite() { + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ + url: '/pages/login/login' + }) + return + } + + const { activity } = this.data + const isFavorited = activity.is_favorited + + try { + if (isFavorited) { + // 取消收藏 + await api.activity.unfavorite(activity.id) + wx.showToast({ + title: '已取消收藏', + icon: 'success' + }) + } else { + // 收藏 + await api.activity.favorite(activity.id) + wx.showToast({ + title: '收藏成功', + icon: 'success' + }) + } + + // 更新状态 + this.setData({ + 'activity.is_favorited': !isFavorited + }) + } catch (err) { + wx.showToast({ + title: err.error || err.message || '操作失败', + icon: 'none' + }) + } + }, + + /** + * 报名活动 + */ + async onSignUp() { + const { activity } = this.data + + // 检查活动状态 + if (activity.status === 'ended' || activity.status === 'full') { + // 如果活动已结束或已满员,弹出二维码引导进群 + const qrCode = activity.activity_guide_qrcode || activity.activityGuideQrcode || this.data.qrcodeImageUrl || 'https://ai-c.maimanji.com/api/common/qrcode?type=group' + + this.setData({ + qrcodeImageUrl: qrCode, + showQrcodeModal: true + }) + return + } + + // 检查登录 + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ + url: '/pages/login/login' + }) + return + } + + // 显示报名确认 + wx.showModal({ + title: '确认报名', + content: activity.is_free + ? '确定要报名参加这个活动吗?' + : `确定要报名参加这个活动吗?需支付 ¥${activity.price}`, + success: (res) => { + if (res.confirm) { + this.handleSignUp() + } + } + }) + }, + + /** + * 处理报名 + */ + async handleSignUp() { + try { + wx.showLoading({ title: '报名中...' }) + + const userInfo = app.globalData.userInfo || {} + const res = await api.activity.signup(this.data.activityId, { + real_name: userInfo.nickname || '', + phone: userInfo.phone || '', + notes: '' + }) + + wx.hideLoading() + + if (res.success) { + wx.showToast({ + title: '报名成功', + icon: 'success' + }) + + // 刷新数据 + this.loadActivityDetail() + this.loadParticipants() + } else { + // 检查是否需要显示二维码(后端开关关闭、活动已结束或已满员) + if (res.code === 'QR_CODE_REQUIRED' || res.error === 'QR_CODE_REQUIRED' || res.code === 'ACTIVITY_ENDED' || res.error === '活动已结束' || res.code === 'ACTIVITY_FULL' || res.error === '活动已满员') { + const { activity } = this.data + if (activity.activity_guide_qrcode || activity.activityGuideQrcode) { + this.setData({ + qrcodeImageUrl: activity.activity_guide_qrcode || activity.activityGuideQrcode, + showQrcodeModal: true + }) + } + if (res.code === 'ACTIVITY_ENDED' || res.error === '活动已结束' || res.code === 'ACTIVITY_FULL' || res.error === '活动已满员') { + const tip = (res.code === 'ACTIVITY_FULL' || res.error === '活动已满员') ? '活动已满员,进群查看更多' : '活动已结束,进群查看更多' + wx.showToast({ title: tip, icon: 'none' }) + } + } else { + wx.showToast({ + title: res.error || '报名失败', + icon: 'none' + }) + } + } + } catch (err) { + wx.hideLoading() + console.error('报名失败', err) + + // 捕获特定错误码以显示二维码 + const isQrRequired = err && (err.code === 'QR_CODE_REQUIRED' || (err.data && err.data.code === 'QR_CODE_REQUIRED')) + const isActivityEnded = err && (err.code === 'ACTIVITY_ENDED' || (err.data && err.data.code === 'ACTIVITY_ENDED') || err.error === '活动已结束') + const isActivityFull = err && (err.code === 'ACTIVITY_FULL' || (err.data && err.data.code === 'ACTIVITY_FULL') || err.error === '活动已满员') + + if (isQrRequired || isActivityEnded || isActivityFull) { + const { activity } = this.data + if (activity.activity_guide_qrcode || activity.activityGuideQrcode) { + this.setData({ + qrcodeImageUrl: activity.activity_guide_qrcode || activity.activityGuideQrcode, + showQrcodeModal: true + }) + } + if (isActivityEnded || isActivityFull) { + const tip = isActivityFull ? '活动已满员,进群查看更多' : '活动已结束,进群查看更多' + wx.showToast({ title: tip, icon: 'none' }) + } + } else { + wx.showToast({ + title: err.error || err.message || '报名失败', + icon: 'none' + }) + } + } + } +}) diff --git a/pages/activity-detail/activity-detail.json b/pages/activity-detail/activity-detail.json new file mode 100644 index 0000000..59832dc --- /dev/null +++ b/pages/activity-detail/activity-detail.json @@ -0,0 +1,9 @@ +{ + "navigationStyle": "custom", + "enablePullDownRefresh": false, + "backgroundTextStyle": "dark", + "backgroundColor": "#F8F8F8", + "usingComponents": { + "app-icon": "../../components/icon/icon" + } +} diff --git a/pages/activity-detail/activity-detail.wxml b/pages/activity-detail/activity-detail.wxml new file mode 100644 index 0000000..c6a180d --- /dev/null +++ b/pages/activity-detail/activity-detail.wxml @@ -0,0 +1,229 @@ + + + + + + + + + + + + + 活动详情 + + + + + + + + + + + + + + + {{statusText}} + + + + + + {{activity.title}} + + + + + 免费活动 + + + ¥ + {{activity.price}} + /人 + + + + {{activity.participants_count}}人已报名 + + + + + + + + + + 活动时间 + {{activity.start_date}} {{activity.start_time}} + + + + + + + + 活动地点 + {{activity.address}} + + + + + + + + 主办方 + {{activity.organizer}} + + + + + + + + 联系电话 + {{activity.contact_phone}} + + + + + + + + 活动详情 + + + + + + + + + + + {{activity.description}} + + + + + + + + + + + + 参与者 ({{participants.length}}) + + 查看全部 + + + + + + + + + {{item.name}} + {{item.join_time}} + + + + + + + + 相关活动推荐 + + + + + {{item.title}} + {{item.start_date}} + + + + + + + + + + + + + + + + {{activity.is_favorited ? '已收藏' : '收藏'}} + + + + + + + + + + + + + + + + + + 参与者列表 + + + + + + + + + + + {{item.name}} + {{item.join_time}} + + + + + + + + + + + + + + + 加入活动群 + 进群获取更多活动资讯,结交志同道合的朋友 + + + + 长按二维码识别或保存 + 保存二维码 + + diff --git a/pages/activity-detail/activity-detail.wxss b/pages/activity-detail/activity-detail.wxss new file mode 100644 index 0000000..e5ca36b --- /dev/null +++ b/pages/activity-detail/activity-detail.wxss @@ -0,0 +1,815 @@ +/* 活动详情页面样式 - 玫瑰紫版 v3.0 */ +page { + background: linear-gradient(180deg, #E8C3D4 0%, #F5E6ED 100%); +} + +.page-container { + min-height: 100vh; + background: linear-gradient(180deg, #E8C3D4 0%, #F5E6ED 100%); + padding-bottom: 180rpx; +} + +/* 固定导航栏容器 */ +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; + background: rgba(248, 249, 252, 0.75); + backdrop-filter: blur(20rpx) saturate(180%); + -webkit-backdrop-filter: blur(20rpx) saturate(180%); +} + +/* 状态栏 */ +.status-bar { + background: transparent; +} + +/* 导航栏 */ +.nav-bar { + display: flex; + align-items: center; + justify-content: center; + padding: 0 32rpx; + background: transparent; + position: relative; +} + +.nav-back { + position: absolute; + left: 32rpx; + top: 50%; + transform: translateY(-50%); + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.back-icon { + width: 48rpx; + height: 48rpx; +} + +.nav-title { + font-size: 36rpx; + font-weight: 700; + color: #1A1A1A; + line-height: 1; +} + +.nav-share { + position: absolute; + right: 32rpx; + top: 50%; + transform: translateY(-50%); + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + background: rgba(255, 255, 255, 0.9); + border-radius: 50%; +} + +.share-icon { + width: 48rpx; + height: 48rpx; + opacity: 0.9; +} + +/* 内容滚动区域 */ +.content-scroll { + height: 100vh; + box-sizing: border-box; +} + +/* 活动封面 - 梦幻渐变占位 */ +.cover-section { + width: 100%; + height: 560rpx; + position: relative; + overflow: hidden; + background: linear-gradient(135deg, #F5E6ED 0%, #FAF5F8 100%); +} + +.cover-image { + width: 100%; + height: 100%; +} + +.status-badge { + position: absolute; + top: 32rpx; + right: 32rpx; + padding: 0 32rpx; + height: 72rpx; + display: flex; + align-items: center; + justify-content: center; + border-radius: 100rpx; + backdrop-filter: blur(10px); +} + +.status-badge.upcoming { + background: linear-gradient(135deg, #60A5FA 0%, #3B82F6 100%); + box-shadow: 0 4rpx 16rpx rgba(96, 165, 250, 0.35); +} + +.status-badge.ongoing { + background: linear-gradient(135deg, #4ADE80 0%, #16A34A 100%); + box-shadow: 0 4rpx 16rpx rgba(74, 222, 128, 0.35); +} + +.status-badge.ended { + background: linear-gradient(135deg, #9CA3AF 0%, #6B7280 100%); + box-shadow: 0 4rpx 16rpx rgba(156, 163, 175, 0.25); +} + +.status-badge.full { + background: linear-gradient(135deg, #F97316 0%, #EA580C 100%); + box-shadow: 0 4rpx 16rpx rgba(249, 115, 22, 0.35); +} + +.status-text { + font-size: 32rpx; + font-weight: 700; + color: #fff; +} + +/* 基本信息区域 - 毛玻璃卡片 */ +.info-section { + margin: 32rpx; + padding: 48rpx; + background: rgba(255, 255, 255, 0.85); + backdrop-filter: blur(16rpx) saturate(150%); + border-radius: 48rpx; + border: 2rpx solid rgba(145, 69, 132, 0.2); + box-shadow: 0 4rpx 20rpx rgba(145, 69, 132, 0.12), + 0 2rpx 8rpx rgba(145, 69, 132, 0.08); +} + +.activity-title { + font-size: 56rpx; + font-weight: 700; + color: #1A1A1A; + line-height: 1.3; + margin-bottom: 32rpx; +} + +/* 价格信息 */ +.price-info { + display: flex; + align-items: center; + justify-content: space-between; + padding: 32rpx 0; + border-bottom: 2rpx solid rgba(145, 69, 132, 0.1); + margin-bottom: 32rpx; +} + +.price-main { + display: flex; + align-items: baseline; + gap: 8rpx; +} + +.price-label { + font-size: 48rpx; + font-weight: 700; + color: #4ADE80; +} + +.price-symbol { + font-size: 40rpx; + font-weight: 700; + color: #F97316; +} + +.price-value { + font-size: 64rpx; + font-weight: 700; + color: #F97316; + line-height: 1; +} + +.price-unit { + font-size: 36rpx; + font-weight: 400; + color: #F97316; +} + +.participants-info { + display: flex; + align-items: center; + gap: 12rpx; + padding: 0 24rpx; + height: 72rpx; + background: rgba(145, 69, 132, 0.1); + border-radius: 100rpx; +} + +.participants-icon { + width: 40rpx; + height: 40rpx; +} + +.participants-text { + font-size: 32rpx; + font-weight: 400; + color: #914584; +} + +/* 信息列表 */ +.info-list { + display: flex; + flex-direction: column; + gap: 32rpx; +} + +.info-item { + display: flex; + align-items: flex-start; + gap: 24rpx; +} + +.info-icon { + width: 48rpx; + height: 48rpx; + margin-top: 4rpx; + flex-shrink: 0; +} + +.info-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.info-label { + font-size: 32rpx; + font-weight: 400; + color: #6A7282; +} + +.info-value { + font-size: 36rpx; + font-weight: 400; + color: #1A1A1A; + line-height: 1.5; +} + +.phone-link { + color: #914584; + text-decoration: underline; +} + +/* 活动详情区域 - 毛玻璃卡片 */ +.detail-section { + margin: 32rpx; + padding: 48rpx; + background: rgba(255, 255, 255, 0.85); + backdrop-filter: blur(16rpx) saturate(150%); + border-radius: 48rpx; + border: 2rpx solid rgba(145, 69, 132, 0.2); + box-shadow: 0 4rpx 20rpx rgba(145, 69, 132, 0.12), + 0 2rpx 8rpx rgba(145, 69, 132, 0.08); +} + +.section-title { + font-size: 48rpx; + font-weight: 700; + color: #1A1A1A; + margin-bottom: 32rpx; +} + +.detail-content { + margin-bottom: 32rpx; +} + +.detail-text { + font-size: 36rpx; + font-weight: 400; + color: #4A5565; + line-height: 1.8; + white-space: pre-wrap; +} + +/* rich-text 样式 */ +.detail-rich-text-wrapper { + position: relative; +} + +.detail-rich-text { + font-size: 36rpx; + line-height: 1.8; + color: #4A5565; + word-break: break-all; + overflow: hidden; +} + +.detail-images { + display: flex; + flex-direction: column; + gap: 24rpx; + overflow: hidden; +} + +.detail-image { + width: 100%; + max-width: 100%; + border-radius: 24rpx; + display: block; +} + +/* 参与者区域 - 毛玻璃卡片 */ +.participants-section { + margin: 32rpx; + padding: 48rpx; + background: rgba(255, 255, 255, 0.85); + backdrop-filter: blur(16rpx) saturate(150%); + border-radius: 48rpx; + border: 2rpx solid rgba(145, 69, 132, 0.2); + box-shadow: 0 4rpx 20rpx rgba(145, 69, 132, 0.12), + 0 2rpx 8rpx rgba(145, 69, 132, 0.08); +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 32rpx; +} + +.view-all { + display: flex; + align-items: center; + gap: 8rpx; +} + +.view-all-text { + font-size: 32rpx; + font-weight: 400; + color: #914584; +} + +.view-all-icon { + width: 32rpx; + height: 32rpx; +} + +.participants-list { + display: flex; + flex-direction: column; + gap: 24rpx; +} + +.participant-item { + display: flex; + align-items: center; + gap: 24rpx; +} + +.participant-avatar { + width: 96rpx; + height: 96rpx; + border-radius: 50%; + border: 2rpx solid rgba(145, 69, 132, 0.2); + background: linear-gradient(135deg, #F5E6ED 0%, #FAF5F8 100%); +} + +.participant-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.participant-name { + font-size: 36rpx; + font-weight: 500; + color: #1A1A1A; +} + +.participant-time { + font-size: 28rpx; + font-weight: 400; + color: #6A7282; +} + +/* 推荐活动区域 - 毛玻璃卡片 */ +.recommend-section { + margin: 32rpx; + padding: 48rpx; + background: rgba(255, 255, 255, 0.85); + backdrop-filter: blur(16rpx) saturate(150%); + border-radius: 48rpx; + border: 2rpx solid rgba(145, 69, 132, 0.2); + box-shadow: 0 4rpx 20rpx rgba(145, 69, 132, 0.12), + 0 2rpx 8rpx rgba(145, 69, 132, 0.08); +} + +.recommend-list { + display: flex; + flex-wrap: wrap; + gap: 20rpx; + margin-top: 24rpx; +} + +.recommend-item { + width: calc((100% - 20rpx) / 2); + background: rgba(245, 230, 237, 0.6); + backdrop-filter: blur(8rpx); + border-radius: 24rpx; + overflow: hidden; + border: 2rpx solid rgba(145, 69, 132, 0.15); + box-sizing: border-box; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; +} + +.recommend-item:active { + transform: translateY(-2rpx) scale(0.98); + box-shadow: 0 2rpx 12rpx rgba(145, 69, 132, 0.15); +} + +.recommend-image { + width: 100%; + height: 180rpx; + background: linear-gradient(135deg, #F5E6ED 0%, #FAF5F8 100%); + display: block; +} + +.recommend-info { + padding: 16rpx; + display: flex; + flex-direction: column; + gap: 8rpx; + background: rgba(255, 255, 255, 0.9); +} + +.recommend-title { + font-size: 26rpx; + font-weight: 500; + color: #1A1A1A; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + line-height: 1.4; + min-height: 72rpx; + word-break: break-all; +} + +.recommend-date { + font-size: 22rpx; + font-weight: 400; + color: #6A7282; + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 底部占位 */ +.bottom-placeholder { + height: 200rpx; +} + +/* 底部操作栏 */ +.bottom-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 198rpx; + background: #fff; + display: flex; + align-items: center; + padding: 0 32rpx; + padding-bottom: env(safe-area-inset-bottom); + box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.04); + z-index: 100; + box-sizing: border-box; +} + +.bar-left { + display: flex; + align-items: center; + gap: 12rpx; +} + +.action-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: transparent; + border: none; + padding: 0; + width: 80rpx; +} + +.share-btn::after { + border: none; +} + +.action-icon { + width: 48rpx; + height: 48rpx; +} + +.action-text { + font-size: 22rpx; + font-weight: 400; + color: #64748b; + margin-top: 4rpx; +} + +.bar-right { + flex: 1; + margin-left: 0rpx; +} + +.signup-btn { + width: 100%; + height: 96rpx; + display: flex; + align-items: center; + justify-content: center; + border-radius: 100rpx; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; +} + +.signup-btn:active { + transform: scale(0.96); +} + +.signup-btn.upcoming { + background: linear-gradient(135deg, #914584 0%, #7A3A6F 100%); + box-shadow: 0 6rpx 24rpx rgba(145, 69, 132, 0.4), + 0 3rpx 12rpx rgba(145, 69, 132, 0.3); +} + +.signup-btn.upcoming:active { + box-shadow: 0 4rpx 16rpx rgba(145, 69, 132, 0.45); +} + +.signup-btn.ongoing { + background: linear-gradient(135deg, #4ADE80 0%, #16A34A 100%); + box-shadow: 0 6rpx 24rpx rgba(74, 222, 128, 0.35); +} + +.signup-btn.ongoing:active { + box-shadow: 0 4rpx 16rpx rgba(74, 222, 128, 0.4); +} + +.signup-btn.ended { + background: linear-gradient(135deg, #9CA3AF 0%, #6B7280 100%); + box-shadow: 0 4rpx 16rpx rgba(156, 163, 175, 0.25); +} + +.signup-btn.full { + background: linear-gradient(135deg, #F97316 0%, #EA580C 100%); + box-shadow: 0 6rpx 24rpx rgba(249, 115, 22, 0.35); +} + +.signup-btn.full:active { + box-shadow: 0 4rpx 16rpx rgba(249, 115, 22, 0.4); +} + +.signup-text { + font-size: 32rpx; + font-weight: 700; + color: #fff; + white-space: nowrap; +} + +/* 参与者列表弹窗 */ +.participants-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(26, 26, 26, 0.5); + z-index: 1000; + display: flex; + align-items: flex-end; + visibility: hidden; + transition: all 0.3s ease; +} + +.participants-modal.show { + visibility: visible; +} + +.participants-modal .modal-content { + width: 100%; + background: #fff; + border-radius: 32rpx 32rpx 0 0; + padding: 40rpx 32rpx; + transform: translateY(100%); + transition: all 0.3s ease; +} + +.participants-modal.show .modal-content { + transform: translateY(0); +} + +/* 二维码引导弹窗 */ +.qrcode-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + visibility: hidden; + opacity: 0; + transition: all 0.3s ease; +} + +.qrcode-modal.show { + visibility: visible; + opacity: 1; +} + +.qrcode-modal .modal-mask { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4rpx); +} + +.qrcode-modal .modal-content { + position: relative; + width: 600rpx; + background: #FFFFFF; + border-radius: 48rpx; + padding: 60rpx 40rpx; + display: flex; + flex-direction: column; + align-items: center; + z-index: 1; + transform: scale(0.8); + transition: all 0.3s ease; +} + +.qrcode-modal.show .modal-content { + transform: scale(1); +} + +.qrcode-modal .close-btn { + position: absolute; + top: 30rpx; + right: 30rpx; + width: 60rpx; + height: 60rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.qrcode-modal .close-icon { + width: 32rpx; + height: 32rpx; +} + +.qrcode-modal .modal-title { + font-size: 40rpx; + font-weight: bold; + color: #333; + margin-bottom: 12rpx; +} + +.qrcode-modal .modal-subtitle { + font-size: 28rpx; + color: #666; + margin-bottom: 40rpx; +} + +.qrcode-modal .qrcode-container { + width: 400rpx; + height: 400rpx; + background: #f9f9f9; + border: 2rpx solid #eee; + border-radius: 24rpx; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 24rpx; + overflow: hidden; +} + +.qrcode-modal .qrcode-image { + width: 360rpx; + height: 360rpx; +} + +.qrcode-modal .modal-tips { + font-size: 24rpx; + color: #999; + margin-bottom: 40rpx; +} + +.qrcode-modal .save-btn { + width: 100%; + height: 88rpx; + background: #07C160; + color: #fff; + border-radius: 44rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + font-weight: bold; +} + +.qrcode-modal .save-btn:active { + opacity: 0.8; +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 48rpx; + border-bottom: 2rpx solid rgba(145, 69, 132, 0.1); +} + +.modal-title { + font-size: 48rpx; + font-weight: 700; + color: #1A1A1A; +} + +.modal-close { + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.close-icon { + width: 48rpx; + height: 48rpx; +} + +.modal-scroll { + flex: 1; + overflow-y: auto; +} + +.modal-participants-list { + padding: 32rpx 48rpx; + display: flex; + flex-direction: column; + gap: 32rpx; +} + +.modal-participant-item { + display: flex; + align-items: center; + gap: 24rpx; +} + +.modal-participant-avatar { + width: 112rpx; + height: 112rpx; + border-radius: 50%; + border: 2rpx solid rgba(145, 69, 132, 0.2); + background: linear-gradient(135deg, #F5E6ED 0%, #FAF5F8 100%); +} + +.modal-participant-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 12rpx; +} + +.modal-participant-name { + font-size: 40rpx; + font-weight: 500; + color: #1A1A1A; +} + +.modal-participant-time { + font-size: 32rpx; + font-weight: 400; + color: #6A7282; +} diff --git a/pages/agreement/agreement.js b/pages/agreement/agreement.js new file mode 100644 index 0000000..367e5d0 --- /dev/null +++ b/pages/agreement/agreement.js @@ -0,0 +1,76 @@ +/** + * 协议页面 + * 显示用户服务协议或隐私协议 + */ +Page({ + data: { + statusBarHeight: 20, + title: '', + content: '' + }, + + onLoad(options) { + const code = options.code || 'user_service' + + // 获取状态栏高度 + const systemInfo = wx.getSystemInfoSync() + this.setData({ + statusBarHeight: systemInfo.statusBarHeight || 20 + }) + + this.loadAgreement(code); + }, + + loadAgreement(code) { + wx.showLoading({ title: '加载中...' }); + + // 优先读取本地缓存 + const cached = wx.getStorageSync(`agreement_${code}`); + const CACHE_DURATION = 60 * 60 * 1000; // 1小时 + const now = Date.now(); + + if (cached && cached.timestamp && (now - cached.timestamp < CACHE_DURATION)) { + this.setData({ + title: cached.data.title, + content: cached.data.content + }); + wx.hideLoading(); + return; + } + + // 网络请求 + wx.request({ + url: `https://ai-c.maimanji.com/api/agreements?code=${code}`, + method: 'GET', + success: (res) => { + wx.hideLoading(); + if (res.data.success) { + const data = res.data.data; + this.setData({ + title: data.title, + content: data.content + }); + + // 写入缓存 + wx.setStorageSync(`agreement_${code}`, { + data: data, + timestamp: now + }); + } else { + wx.showToast({ title: '协议不存在', icon: 'none' }); + } + }, + fail: (err) => { + wx.hideLoading(); + wx.showToast({ title: '加载失败', icon: 'none' }); + } + }); + }, + + /** + * 返回上一页 + */ + goBack() { + wx.navigateBack() + } +}) diff --git a/pages/agreement/agreement.json b/pages/agreement/agreement.json new file mode 100644 index 0000000..e90e996 --- /dev/null +++ b/pages/agreement/agreement.json @@ -0,0 +1,4 @@ +{ + "usingComponents": {}, + "navigationStyle": "custom" +} diff --git a/pages/agreement/agreement.wxml b/pages/agreement/agreement.wxml new file mode 100644 index 0000000..c639776 --- /dev/null +++ b/pages/agreement/agreement.wxml @@ -0,0 +1,20 @@ + + + + + + + + + {{title}} + + + + + + + + + + + diff --git a/pages/agreement/agreement.wxss b/pages/agreement/agreement.wxss new file mode 100644 index 0000000..3ab1c5f --- /dev/null +++ b/pages/agreement/agreement.wxss @@ -0,0 +1,64 @@ +/* 协议页面样式 */ +.agreement-page { + min-height: 100vh; + background: #fff; +} + +/* 导航栏 */ +.nav-bar { + position: fixed; + top: 0; + left: 0; + right: 0; + background: #fff; + z-index: 100; + border-bottom: 1rpx solid #f0f0f0; +} + +.nav-content { + height: 88rpx; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 32rpx; +} + +.back-btn { + width: 72rpx; + height: 72rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.back-icon { + width: 40rpx; + height: 40rpx; +} + +.nav-title { + font-size: 36rpx; + font-weight: bold; + color: #1e2939; +} + +.nav-placeholder { + width: 72rpx; +} + +/* 内容区域 */ +.content-scroll { + height: 100vh; +} + +.content-wrapper { + padding: 32rpx; + padding-bottom: 100rpx; +} + +.content-text { + font-size: 28rpx; + color: #4a5565; + line-height: 1.8; + white-space: pre-wrap; +} diff --git a/pages/backpack/backpack.js b/pages/backpack/backpack.js new file mode 100644 index 0000000..eb35140 --- /dev/null +++ b/pages/backpack/backpack.js @@ -0,0 +1,54 @@ +const { request } = require('../../utils_new/request'); + +Page({ + data: { + statusBarHeight: 20, + navBarHeight: 44, + totalNavHeight: 64, + loading: true, + items: [] + }, + onLoad() { + const sys = wx.getSystemInfoSync(); + const menu = wx.getMenuButtonBoundingClientRect(); + const statusBarHeight = sys.statusBarHeight || 20; + const navBarHeight = menu.height + (menu.top - statusBarHeight) * 2; + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight: statusBarHeight + navBarHeight + }); + this.load(); + }, + onBack() { + wx.navigateBack({ delta: 1 }); + }, + async load() { + this.setData({ loading: true }); + try { + try { + const res = await request({ url: '/api/backpack', method: 'GET' }); + const body = res.data || {}; + if (body.code !== 0) throw new Error(body.message || '加载失败'); + const items = (body.data?.items || body.data || []).map((x) => ({ + id: x.id || x.item_id || '', + name: x.name || x.item_name || '', + quantity: Number(x.quantity || 0), + image_url: x.image_url || x.imageUrl || '' + })); + this.setData({ items }); + } catch (err) { + console.log('API failed, using mock data'); + this.setData({ + items: [ + { id: 1, name: '新手礼包', quantity: 1, image_url: '' }, + { id: 2, name: '加速卡', quantity: 5, image_url: '' } + ] + }); + } + } finally { + this.setData({ loading: false }); + } + } +}); + diff --git a/pages/backpack/backpack.json b/pages/backpack/backpack.json new file mode 100644 index 0000000..3153ca5 --- /dev/null +++ b/pages/backpack/backpack.json @@ -0,0 +1,5 @@ +{ + "usingComponents": { + "app-icon": "/components/icon/icon" + } +} diff --git a/pages/backpack/backpack.wxml b/pages/backpack/backpack.wxml new file mode 100644 index 0000000..f2ff0d5 --- /dev/null +++ b/pages/backpack/backpack.wxml @@ -0,0 +1,31 @@ + + + + + + + + 背包物品 + + + + + + 加载中... + 暂无物品 + + + + + + + + + {{item.name}} + x{{item.quantity}} + + + + + + diff --git a/pages/backpack/backpack.wxss b/pages/backpack/backpack.wxss new file mode 100644 index 0000000..c5113b3 --- /dev/null +++ b/pages/backpack/backpack.wxss @@ -0,0 +1,121 @@ +.page { + min-height: 100vh; + background: #E8C3D4; +} + +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; + background: rgba(248, 249, 252, 0.75); + backdrop-filter: blur(20rpx) saturate(180%); + -webkit-backdrop-filter: blur(20rpx) saturate(180%); +} + +.status-bar { + background: transparent; +} + +.nav-bar { + display: flex; + align-items: center; + justify-content: center; + padding: 0 32rpx; + position: relative; +} + +.nav-back { + position: absolute; + left: 32rpx; + top: 50%; + transform: translateY(-50%); + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.back-icon { + width: 48rpx; + height: 48rpx; +} + +.nav-title { + font-size: 34rpx; + font-weight: 700; + color: #1A1A1A; +} + +.wrap { + padding: 24rpx; +} + +.card { + background: #ffffff; + border-radius: 40rpx; + padding: 24rpx; + box-shadow: 0 10rpx 20rpx rgba(17, 24, 39, 0.04); +} + +.loading, +.empty { + text-align: center; + color: #9ca3af; + font-weight: 800; + padding: 80rpx 0; +} + +.grid { + display: flex; + flex-wrap: wrap; + gap: 16rpx; +} + +.item { + width: calc(33.333% - 11rpx); + border: 2rpx solid #f3f4f6; + border-radius: 24rpx; + padding: 16rpx; +} + +.thumb { + height: 160rpx; + border-radius: 18rpx; + overflow: hidden; + background: #f9fafb; + display: flex; + align-items: center; + justify-content: center; +} + +.img { + width: 100%; + height: 100%; +} + +.img-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.name { + margin-top: 12rpx; + font-size: 24rpx; + font-weight: 900; + color: #111827; +} + +.count { + margin-top: 6rpx; + font-size: 22rpx; + color: #6b7280; + font-weight: 700; +} + diff --git a/pages/brand/brand.js b/pages/brand/brand.js new file mode 100644 index 0000000..b4eeb00 --- /dev/null +++ b/pages/brand/brand.js @@ -0,0 +1,51 @@ +const api = require('../../utils/api') + +Page({ + data: { + sections: [], + loading: true, + error: null + }, + + onLoad() { + this.loadBrandInfo() + }, + + onBack() { + wx.navigateBack() + }, + + async loadBrandInfo() { + this.setData({ loading: true, error: null }) + + try { + const res = await api.common.getBrandConfig() + console.log('[brand] 品牌配置响应:', res) + + if (res.success && res.data) { + const data = res.data + const sections = [] + + if (data.about_brand && data.about_brand.value) { + sections.push({ title: '关于品牌', content: data.about_brand.value }) + } + if (data.company_intro && data.company_intro.value) { + sections.push({ title: '公司简介', content: data.company_intro.value }) + } + if (data.contact_info && data.contact_info.value) { + sections.push({ title: '联系我们', content: data.contact_info.value }) + } + + this.setData({ sections }) + console.log('[brand] 品牌信息sections:', this.data.sections) + } else { + this.setData({ error: res.error || '加载失败' }) + } + } catch (err) { + console.error('[brand] 加载品牌信息失败:', err) + this.setData({ error: err.message || '加载失败' }) + } finally { + this.setData({ loading: false }) + } + } +}) diff --git a/pages/brand/brand.json b/pages/brand/brand.json new file mode 100644 index 0000000..8835af0 --- /dev/null +++ b/pages/brand/brand.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/pages/brand/brand.wxml b/pages/brand/brand.wxml new file mode 100644 index 0000000..e7f1f55 --- /dev/null +++ b/pages/brand/brand.wxml @@ -0,0 +1,39 @@ + + + + + + 返回 + + 关于品牌 + + + + + + + 加载中... + + + + + {{error}} + 点击重试 + + + + + + + + + + + + + + + 暂无品牌信息 + + + diff --git a/pages/brand/brand.wxss b/pages/brand/brand.wxss new file mode 100644 index 0000000..f20896b --- /dev/null +++ b/pages/brand/brand.wxss @@ -0,0 +1,106 @@ +.page { + min-height: 100vh; + background-color: #f8f8f8; + display: flex; + flex-direction: column; +} + +.unified-header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 194rpx; + background: linear-gradient(135deg, #B06AB3 0%, #9B59B6 100%); + border-bottom: none; + display: flex; + align-items: flex-end; + justify-content: space-between; + padding: 0 32rpx 20rpx; + z-index: 1000; +} + +.unified-header-left { + display: flex; + align-items: center; + gap: 8rpx; + width: 160rpx; + height: 56rpx; +} + +.unified-back-icon { + width: 56rpx; + height: 56rpx; +} + +.unified-back-text { + font-size: 34rpx; + font-weight: bold; + color: #ffffff; +} + +.unified-header-title { + font-size: 40rpx; + font-weight: bold; + color: #ffffff; + flex: 1; + text-align: center; +} + +.unified-header-right { + width: 160rpx; + height: 56rpx; +} + +.content { + flex: 1; +} + +.loading-tip, .error-tip, .empty-tip { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: 200rpx; +} + +.loading-tip text, .empty-tip text, .error-tip text { + font-size: 28rpx; + color: #999; +} + +.error-tip text:first-child { + color: #ff6b6b; + margin-bottom: 20rpx; +} + +.retry-btn { + background: linear-gradient(135deg, #B06AB3 0%, #9B59B6 100%); + color: #fff !important; + padding: 16rpx 48rpx; + border-radius: 36rpx; + font-size: 28rpx !important; +} + +.empty-tip image { + width: 240rpx; + height: 240rpx; + margin-bottom: 20rpx; +} + +.brand-content { + padding: 30rpx; +} + +.section-item { + background: #fff; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 30rpx; +} + +.section-item rich-text { + font-size: 28rpx; + color: #666; + line-height: 1.8; +} diff --git a/pages/character-detail/character-detail.js b/pages/character-detail/character-detail.js new file mode 100644 index 0000000..87d93bf --- /dev/null +++ b/pages/character-detail/character-detail.js @@ -0,0 +1,657 @@ +// pages/character-detail/character-detail.js +// 角色详情页面 - 对接后端API + +const app = getApp() +const api = require('../../utils/api') +const config = require('../../config/index') + +// 获取静态资源基础URL(去掉/api后缀) +const getStaticBaseUrl = () => { + const apiUrl = config.API_BASE_URL + return apiUrl.replace(/\/api$/, '') +} + +Page({ + data: { + loading: true, + character: null, + isLiked: false, + isPlaying: false, // 音频播放状态 + + // 爱心弹窗 + showHeartPopup: false, + userLovePoints: 0, // 用户爱心值 + purchasing: false, + unlockHeartsCost: 500 // 默认解锁爱心成本 + }, + + onLoad(options) { + const characterId = options.id + if (characterId) { + this.loadCharacterDetail(characterId) + this.loadHeartBalance() + this.loadUnlockConfig(characterId) + } else { + wx.showToast({ title: '参数错误', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + } + }, + + /** + * 加载解锁配置 + */ + async loadUnlockConfig(characterId) { + try { + const res = await api.chat.getQuota(characterId) + if (res.success && res.data && res.data.unlock_config) { + const cost = res.data.unlock_config.hearts_cost + if (typeof cost === 'number') { + this.setData({ + unlockHeartsCost: cost + }) + console.log('[character-detail] 已从后端同步解锁成本:', cost) + } + } + } catch (err) { + console.log('[character-detail] 加载解锁配置失败,使用默认值', err) + } + }, + + onShow() { + // 刷新爱心余额 + this.loadHeartBalance() + }, + + onUnload() { + // 清理音频资源 + if (this.audioContext) { + this.audioContext.stop() + this.audioContext.destroy() + this.audioContext = null + } + }, + + /** + * 加载角色详情 + */ + async loadCharacterDetail(id) { + this.setData({ loading: true }) + + try { + const res = await api.character.getDetail(id) + + console.log('[character-detail] API返回原始数据:', JSON.stringify(res)) + + // 兼容两种返回格式 + let data = null + if (res.code === 0 && res.data) { + data = res.data + } else if (res.success && res.data) { + data = res.data + } + + if (data) { + // 打印关键字段 + console.log('[character-detail] greetingAudioUrl:', data.greetingAudioUrl) + console.log('[character-detail] greeting_audio_url:', data.greeting_audio_url) + console.log('[character-detail] audio_url:', data.audio_url) + + const character = this.transformCharacter(data) + + console.log('[character-detail] 转换后的audioUrl:', character.audioUrl) + + this.setData({ + character, + isLiked: data.is_liked || false, + loading: false + }) + } else { + throw new Error(res.message || '加载失败') + } + } catch (err) { + console.error('加载角色详情失败', err) + this.setData({ loading: false }) + wx.showToast({ title: '加载失败', icon: 'none' }) + } + }, + + /** + * 转换角色数据格式 + */ + transformCharacter(data) { + // 静态资源基础URL + const staticBaseUrl = getStaticBaseUrl() + + console.log('[character-detail transformCharacter] 原始数据:', { + age: data.age, + companion_type: data.companion_type, + name: data.name + }) + + // 转换照片路径为完整URL + const convertPhotoUrl = (url) => { + if (!url) return '' + // 如果已经是完整URL,直接返回 + if (url.startsWith('http://') || url.startsWith('https://')) { + return url + } + // 如果是相对路径,拼接基础URL + if (url.startsWith('/characters/')) { + return staticBaseUrl + url + } + return url + } + + // 使用后端返回的短标签格式爱好,或者自己处理 + let hobbies = data.hobbiesTags || [] + if (hobbies.length === 0 && data.hobbies) { + // 如果后端没有返回hobbiesTags,自己处理 + if (typeof data.hobbies === 'string') { + hobbies = [data.hobbies.substring(0, 4)] + } else if (Array.isArray(data.hobbies)) { + hobbies = data.hobbies.map(h => String(h).substring(0, 4)).slice(0, 5) + } else if (typeof data.hobbies === 'object') { + const allHobbies = data.hobbies.adult || Object.values(data.hobbies).flat() + hobbies = allHobbies.map(h => { + // 提取短标签:去除括号内容,截取前4个字符 + let tag = String(h).replace(/[((][^))]*[))]/g, '').trim() + const parts = tag.split(/[、,,;;::]/) + tag = parts[0].trim() + return tag.length > 4 ? tag.substring(0, 4) : tag + }).filter(t => t.length > 0).slice(0, 5) + } + } + + // 解析性格特点(也提取短标签) + let traits = data.traits || [] + if (!Array.isArray(traits) || traits.length === 0) { + if (data.personalityTraits) { + if (Array.isArray(data.personalityTraits)) { + traits = data.personalityTraits.map(t => String(t).substring(0, 4)).slice(0, 5) + } else if (typeof data.personalityTraits === 'object') { + const allTraits = Object.values(data.personalityTraits).flat() + traits = allTraits.map(t => String(t).substring(0, 4)).slice(0, 5) + } + } + } + + // 相册:优先使用后端返回的gallery,转换为完整URL + let photos = (data.gallery || []).map(convertPhotoUrl).filter(p => p) + if (photos.length === 0) { + // 如果没有相册,使用宣传图或头像作为相册 + const promoImage = convertPhotoUrl(data.promoImage || data.promo_image) + const avatar = convertPhotoUrl(data.avatar || data.image) + if (promoImage) { + photos = [promoImage] + } else if (avatar) { + photos = [avatar] + } + } + + // 头像:转换为完整URL(用于小头像显示) + const avatar = convertPhotoUrl(data.avatar || data.logo || data.image) || '' + + // 宣传图:转换为完整URL(用于头部大图显示) + const promoImage = convertPhotoUrl(data.promoImage || data.promo_image || data.avatar || data.image) || '' + + // 处理年龄显示:如果已包含"岁"则直接使用,否则添加"岁" + let ageDisplay = '' + if (data.age) { + const ageStr = String(data.age).trim() + // 如果age不为空且不是'null'字符串 + if (ageStr && ageStr !== 'null' && ageStr !== 'undefined') { + ageDisplay = ageStr.includes('岁') ? ageStr : ageStr + '岁' + } + } + + // 如果没有年龄,尝试从companion_type中提取 + if (!ageDisplay && data.companion_type) { + const ageMatch = data.companion_type.match(/(\d+岁)/) + if (ageMatch) { + ageDisplay = ageMatch[1] + } + } + + console.log('[character-detail transformCharacter] 年龄处理结果:', { + 原始age: data.age, + 最终ageDisplay: ageDisplay + }) + + return { + id: data.id, + name: data.name, + avatar: avatar, + promoImage: promoImage, // 宣传图(用于头部大图) + job: data.occupation || data.companionType || '', + age: data.age || '', + ageDisplay: ageDisplay, + location: data.location || data.province || '', + audioDuration: data.audio_duration || '12"', + about: data.about || data.selfIntroduction || data.bio || '', + traits: traits.slice(0, 5), + hobbies: hobbies.slice(0, 5), + photos: photos, + voiceId: data.voice_id || data.voiceFeatures, + audioUrl: data.greetingAudioUrl || data.greeting_audio_url || data.audio_url || '', // 预录制的开场白音频URL + // Edge TTS 配置(用于实时生成语音) + edgeTtsVoice: data.edgeTtsVoice || data.edge_tts_voice || '', + edgeTtsRate: data.edgeTtsRate || data.edge_tts_rate || '', + edgeTtsPitch: data.edgeTtsPitch || data.edge_tts_pitch || '' + } + }, + + // 返回上一页 + goBack() { + wx.navigateBack() + }, + + // 播放音频 + async onPlayAudio() { + const { character, isPlaying } = this.data + + // 防止重复点击 + if (isPlaying) { + // 如果正在播放,点击则停止 + if (this.audioContext) { + try { + this.audioContext.stop() + } catch (e) {} + } + this.setData({ isPlaying: false }) + return + } + + // 检查是否有有效的音频URL(非空字符串) + const audioUrl = character.audioUrl + console.log('[character-detail] audioUrl:', audioUrl, '类型:', typeof audioUrl) + + if (audioUrl && audioUrl.trim() !== '') { + // 处理相对路径,拼接完整URL + let fullAudioUrl = audioUrl + if (audioUrl.startsWith('/')) { + fullAudioUrl = getStaticBaseUrl() + audioUrl + } + console.log('[character-detail] 完整音频URL:', fullAudioUrl) + + // 先检查文件是否存在 + wx.request({ + url: fullAudioUrl, + method: 'HEAD', + success: (res) => { + console.log('[character-detail] HEAD请求结果:', res.statusCode) + if (res.statusCode === 200) { + this.playAudioUrl(fullAudioUrl) + } else { + wx.showToast({ title: '音频文件不存在', icon: 'none' }) + } + }, + fail: (err) => { + console.log('[character-detail] HEAD请求失败,尝试直接播放:', err) + // 有些服务器不支持HEAD,直接尝试播放 + this.playAudioUrl(fullAudioUrl) + } + }) + return + } + + // 没有预录制音频,提示用户 + wx.showToast({ + title: '该角色暂无独白音频', + icon: 'none', + duration: 2000 + }) + }, + + // 播放Base64格式的音频 + playBase64Audio(base64Data) { + // 将Base64转换为临时文件 + const fs = wx.getFileSystemManager() + const filePath = `${wx.env.USER_DATA_PATH}/temp_audio_${Date.now()}.mp3` + + try { + // 解码Base64并写入文件 + fs.writeFileSync(filePath, base64Data, 'base64') + + // 播放音频 + this.playAudioUrl(filePath) + } catch (err) { + console.error('写入音频文件失败:', err) + wx.showToast({ title: '播放失败', icon: 'none' }) + } + }, + + // 播放音频URL + playAudioUrl(url) { + console.log('[character-detail] playAudioUrl 开始播放:', url) + + // 如果正在播放,先停止 + if (this.audioContext) { + try { + this.audioContext.stop() + this.audioContext.destroy() + } catch (e) { + console.log('[character-detail] 停止旧音频时出错:', e) + } + this.audioContext = null + } + + // 显示播放中提示 + wx.showToast({ + title: '播放独白中...', + icon: 'none', + duration: 5000 + }) + + // 延迟创建新的音频上下文,避免冲突 + setTimeout(() => { + // 不使用 useWebAudioImplement,某些情况下可能导致问题 + const innerAudioContext = wx.createInnerAudioContext() + this.audioContext = innerAudioContext + + // 设置音量为最大 + innerAudioContext.volume = 1.0 + innerAudioContext.src = url + innerAudioContext.obeyMuteSwitch = false // 不受静音开关影响 + + innerAudioContext.onCanplay(() => { + console.log('[character-detail] 音频可以播放了, duration:', innerAudioContext.duration) + }) + + innerAudioContext.onPlay(() => { + console.log('[character-detail] 音频开始播放, volume:', innerAudioContext.volume) + this.setData({ isPlaying: true }) + }) + + innerAudioContext.onTimeUpdate(() => { + // 每秒打印一次进度,确认音频在播放 + const currentTime = Math.floor(innerAudioContext.currentTime) + if (currentTime !== this._lastLogTime) { + console.log('[character-detail] 播放进度:', currentTime, '/', Math.floor(innerAudioContext.duration || 0)) + this._lastLogTime = currentTime + } + }) + + innerAudioContext.onError((err) => { + console.error('[character-detail] 音频播放错误:', JSON.stringify(err)) + this.setData({ isPlaying: false }) + wx.hideToast() + + // 针对不同错误给出提示 + let errMsg = '播放失败' + if (err.errCode === 10001) { + errMsg = '系统错误,请重试' + } else if (err.errCode === 10002) { + errMsg = '网络错误' + } else if (err.errCode === 10003) { + errMsg = '音频文件错误' + } else if (err.errCode === 10004) { + errMsg = '音频格式不支持' + } else if (err.errMsg && err.errMsg.includes('interruption')) { + errMsg = '播放被中断,请重试' + } + + wx.showToast({ title: errMsg, icon: 'none' }) + }) + + innerAudioContext.onEnded(() => { + console.log('[character-detail] 音频播放结束') + this.setData({ isPlaying: false }) + wx.hideToast() + // 清理临时文件 + if (url.startsWith(wx.env.USER_DATA_PATH)) { + try { + wx.getFileSystemManager().unlinkSync(url) + } catch (e) { + // 忽略删除失败 + } + } + }) + + // 延迟播放,确保音频上下文准备好 + setTimeout(() => { + console.log('[character-detail] 调用 play()') + innerAudioContext.play() + }, 100) + }, 50) + }, + + // 查看全部相册 + onViewAllPhotos() { + const { photos } = this.data.character + if (photos && photos.length > 0) { + wx.previewImage({ + urls: photos, + current: photos[0] + }) + } + }, + + // 预览单张照片 + onPreviewPhoto(e) { + const index = e.currentTarget.dataset.index + const { photos } = this.data.character + if (photos && photos.length > 0) { + wx.previewImage({ + urls: photos, + current: photos[index] + }) + } + }, + + // 不喜欢 - 直接返回上一页 + onDislike() { + wx.navigateBack() + }, + + // 喜欢/取消喜欢 + async onLike() { + const { character, isLiked } = this.data + + // 检查登录 + if (app.checkNeedLogin && app.checkNeedLogin()) return + + try { + const res = await api.character.toggleLike(character.id) + + if (res.success) { + const newLiked = !isLiked + this.setData({ isLiked: newLiked }) + // 静默操作,不显示提示 + } + } catch (err) { + console.error('喜欢操作失败', err) + wx.showToast({ title: '操作失败', icon: 'none' }) + } + }, + + // 聊天 + onChat() { + const { character } = this.data + wx.navigateTo({ + url: `/pages/chat-detail/chat-detail?id=${character.id}&name=${encodeURIComponent(character.name)}` + }) + }, + + // ==================== 爱心弹窗相关 ==================== + + /** + * 加载用户爱心值 + * 使用 /api/auth/me 接口,该接口从 im_users.grass_balance 读取余额 + */ + async loadHeartBalance() { + try { + const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) + if (!token) { + this.setData({ userLovePoints: 0 }) + return + } + + const res = await api.auth.getCurrentUser() + if (res.success && res.data) { + this.setData({ + userLovePoints: res.data.grass_balance || 0 + }) + console.log('[character-detail] 爱心值加载成功:', res.data.grass_balance) + } + } catch (err) { + console.log('加载爱心值失败', err) + } + }, + + /** + * 显示爱心弹窗 + */ + showHeartPopup() { + this.setData({ showHeartPopup: true }) + }, + + /** + * 关闭爱心弹窗 + */ + closeHeartPopup() { + this.setData({ showHeartPopup: false }) + }, + + /** + * 阻止事件冒泡 + */ + preventBubble() {}, + + /** + * 阻止滚动穿透 + */ + preventTouchMove() {}, + + /** + * 分享解锁 + */ + onShareUnlock() { + wx.showToast({ + title: '分享功能开发中', + icon: 'none' + }) + // TODO: 实现分享解锁逻辑 + }, + + /** + * 爱心兑换 + */ + async onHeartExchange() { + const { character, userLovePoints, unlockHeartsCost } = this.data + + // 检查登录 + const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) + if (!token) { + wx.showToast({ title: '请先登录', icon: 'none' }) + setTimeout(() => { + wx.navigateTo({ url: '/pages/login/login' }) + }, 1500) + return + } + + // 检查爱心值是否足够 + if (userLovePoints < unlockHeartsCost) { + wx.showModal({ + title: '爱心值不足', + content: `您的爱心值不足${unlockHeartsCost},是否前往充值?`, + confirmText: '去充值', + success: (res) => { + if (res.confirm) { + wx.navigateTo({ url: '/pages/recharge/recharge' }) + } + } + }) + return + } + + // 确认兑换 + wx.showModal({ + title: '确认兑换', + content: `使用${unlockHeartsCost}爱心值解锁与${character.name}的聊天?`, + confirmText: '确认兑换', + success: async (res) => { + if (res.confirm) { + await this.doHeartExchange() + } + } + }) + }, + + /** + * 执行爱心兑换 + */ + async doHeartExchange() { + const { character, unlockHeartsCost, userLovePoints } = this.data + + this.setData({ purchasing: true }) + wx.showLoading({ title: '兑换中...' }) + + try { + // 调用角色解锁API + const res = await api.character.unlock({ + character_id: character.id, + unlock_type: 'hearts' + }) + + wx.hideLoading() + + if (res.success) { + wx.showToast({ title: '解锁成功', icon: 'success' }) + this.setData({ showHeartPopup: false }) + + // 更新本地爱心余额(优先使用后端返回,否则本地计算) + const newBalance = res.data?.remaining_hearts ?? (userLovePoints - unlockHeartsCost) + this.setData({ + userLovePoints: newBalance + }) + + // 延迟后跳转到聊天页面 + setTimeout(() => { + wx.navigateTo({ + url: `/pages/chat-detail/chat-detail?id=${character.id}&name=${encodeURIComponent(character.name)}` + }) + }, 1000) + } else { + wx.showToast({ title: res.message || '兑换失败', icon: 'none' }) + } + } catch (err) { + wx.hideLoading() + console.error('兑换失败', err) + wx.showToast({ title: '网络错误,请重试', icon: 'none' }) + } finally { + this.setData({ purchasing: false }) + } + }, + + /** + * 选择爱心套餐(已废弃,保留兼容) + */ + selectHeartPackage(e) { + const index = e.currentTarget.dataset.index + this.setData({ selectedHeartPackage: index }) + }, + + /** + * 购买并解锁角色聊天(已废弃,保留兼容) + * 使用 /api/payment/unified-order 接口 + * 测试模式下返回 testMode: true,订单直接完成,无需调用微信支付 + */ + async buyAndUnlock() { + // 调用新的爱心兑换逻辑 + await this.onHeartExchange() + }, + + /** + * 跳转到用户协议 + */ + goToUserAgreement() { + wx.navigateTo({ url: '/pages/agreement/agreement?code=user-agreement' }) + }, + + /** + * 跳转到隐私政策 + */ + goToPrivacyPolicy() { + wx.navigateTo({ url: '/pages/agreement/agreement?code=privacy-policy' }) + } +}) diff --git a/pages/character-detail/character-detail.json b/pages/character-detail/character-detail.json new file mode 100644 index 0000000..e90e996 --- /dev/null +++ b/pages/character-detail/character-detail.json @@ -0,0 +1,4 @@ +{ + "usingComponents": {}, + "navigationStyle": "custom" +} diff --git a/pages/character-detail/character-detail.wxml b/pages/character-detail/character-detail.wxml new file mode 100644 index 0000000..ec7a779 --- /dev/null +++ b/pages/character-detail/character-detail.wxml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + {{character.name}} + {{character.job}} + + + + + {{character.ageDisplay}} + {{character.location}} + + + + + + + + + {{isPlaying ? '播放独白中...' : '收听独白'}} + + + + + {{character.audioDuration || '12"'}} + + + + + + 相册 + 查看全部 + + + + + + +{{character.photos.length - 2}} + + + + + + + + 关于我 + {{character.about}} + + + + + 兴趣爱好 + + {{item}} + ♥ {{item}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 解锁与{{character.name}}的专属聊天 + + + + + + + + + + + + + + + {{unlockHeartsCost}}爱心 + {{userLovePoints >= unlockHeartsCost ? '余额充足 立即兑换' : '爱心值不足 去充值'}} + + + 兑换 + + + + + + + + + + diff --git a/pages/character-detail/character-detail.wxss b/pages/character-detail/character-detail.wxss new file mode 100644 index 0000000..0a804ec --- /dev/null +++ b/pages/character-detail/character-detail.wxss @@ -0,0 +1,690 @@ +/* 人物详情页样式 */ +.page-container { + min-height: 100vh; + background: #fff; + position: relative; + overflow-x: hidden; +} + +/* 顶部大图区域 */ +.hero-section { + position: relative; + width: 100%; + height: 580rpx; +} + +.hero-image { + width: 100%; + height: 100%; +} + +.hero-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 200rpx; + background: linear-gradient(to bottom, rgba(0,0,0,0.3), transparent); + padding: 96rpx 32rpx 0; +} + +.back-btn { + width: 72rpx; + height: 72rpx; + background: rgba(255,255,255,0.25); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.back-icon { + width: 48rpx; + height: 48rpx; + filter: brightness(0) invert(1); +} + +/* 内容卡片区域 */ +.content-card { + position: relative; + margin-top: -60rpx; + background: #fff; + border-radius: 48rpx 48rpx 0 0; + min-height: calc(100vh - 520rpx); + padding: 36rpx 32rpx; + box-shadow: 0 -10rpx 40rpx rgba(0,0,0,0.08); + height: calc(100vh - 520rpx); + width: 100%; + box-sizing: border-box; + overflow-x: hidden; +} + +/* 基本信息 */ +.profile-header { + margin-bottom: 16rpx; +} + +.profile-name { + display: block; + font-size: 48rpx; + font-weight: 700; + color: #101828; + line-height: 1.3; + margin-bottom: 4rpx; +} + +.profile-job { + display: block; + font-size: 28rpx; + font-weight: 500; + color: #6a7282; +} + +/* 标签 */ +.profile-tags { + display: flex; + gap: 16rpx; + margin-bottom: 24rpx; +} + +.tag { + background: #f3f4f6; + border-radius: 24rpx; + padding: 12rpx 24rpx; + font-size: 26rpx; + font-weight: 600; + color: #364153; +} + +/* 收听独白 */ +.audio-section { + display: flex; + align-items: center; + gap: 24rpx; + background: #f9fafb; + border: 2rpx solid #f3f4f6; + border-radius: 24rpx; + padding: 20rpx 24rpx; + margin-bottom: 28rpx; + transition: all 0.3s ease; +} + +/* 播放中状态 */ +.audio-section.playing { + background: linear-gradient(135deg, #e8f5e9 0%, #f1f8e9 100%); + border-color: #4caf50; + box-shadow: 0 4rpx 12rpx rgba(76, 175, 80, 0.15); +} + +.audio-btn { + width: 80rpx; + height: 80rpx; + background: #fff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.08); + flex-shrink: 0; + transition: all 0.3s ease; +} + +/* 播放中按钮样式 */ +.audio-section.playing .audio-btn { + background: linear-gradient(135deg, #4caf50 0%, #66bb6a 100%); + box-shadow: 0 4rpx 12rpx rgba(76, 175, 80, 0.3); + animation: pulse 1.5s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } +} + +.audio-icon { + width: 40rpx; + height: 40rpx; + opacity: 1; + transition: all 0.3s ease; +} + +/* 播放中图标样式 */ +.audio-section.playing .audio-icon { + filter: brightness(0) invert(1); + animation: rotate 2s linear infinite; +} + +@keyframes rotate { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.audio-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 6rpx; +} + +.audio-label { + font-size: 26rpx; + font-weight: 600; + color: #99a1af; + transition: color 0.3s ease; +} + +/* 播放中文字样式 */ +.audio-section.playing .audio-label { + color: #4caf50; + font-weight: 700; +} + +.audio-wave { + display: flex; + gap: 4rpx; + height: 6rpx; +} + +.wave-bar { + flex: 1; + height: 100%; + background: #d1d5dc; + border-radius: 100rpx; + transition: all 0.3s ease; +} + +/* 播放中波形条动画 */ +.wave-bar.animating { + background: #4caf50; + animation: wave 1s ease-in-out infinite; +} + +.wave-bar.animating:nth-child(2n) { + animation-delay: 0.1s; +} + +.wave-bar.animating:nth-child(3n) { + animation-delay: 0.2s; +} + +.wave-bar.animating:nth-child(4n) { + animation-delay: 0.3s; +} + +@keyframes wave { + 0%, 100% { + height: 6rpx; + opacity: 0.5; + } + 50% { + height: 12rpx; + opacity: 1; + } +} + +.audio-duration { + font-size: 26rpx; + font-weight: 600; + color: #99a1af; + flex-shrink: 0; + transition: color 0.3s ease; +} + +/* 播放中时长样式 */ +.audio-section.playing .audio-duration { + color: #4caf50; +} + +/* 通用区块 */ +.section { + margin-bottom: 24rpx; +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16rpx; +} + +.section-title { + display: block; + font-size: 30rpx; + font-weight: 700; + color: #101828; + margin-bottom: 12rpx; +} + +.section-header .section-title { + margin-bottom: 0; +} + +.section-content { + font-size: 28rpx; + color: #4a5565; + line-height: 1.5; +} + +.view-all { + font-size: 26rpx; + font-weight: 600; + color: #ff6b6b; +} + +/* 兴趣爱好标签 */ +.hobby-tags { + display: flex; + flex-wrap: wrap; + gap: 12rpx; + width: 100%; + box-sizing: border-box; +} + +.hobby-tag { + background: #f9fafb; + border: 2rpx solid #e5e7eb; + border-radius: 24rpx; + padding: 10rpx 24rpx; + font-size: 26rpx; + font-weight: 600; + color: #4a5565; + flex-shrink: 0; +} + +.hobby-tag.highlight { + background: #fff0f0; + border-color: #ff6b6b; + color: #ff6b6b; +} + +/* 相册 */ +.photo-grid { + display: flex; + gap: 16rpx; +} + +.photo-item { + flex: 1; + height: 280rpx; + border-radius: 24rpx; + overflow: hidden; + position: relative; +} + +.photo-image { + width: 100%; + height: 100%; +} + +.photo-overlay { + position: absolute; + inset: 0; + background: rgba(0,0,0,0.2); + display: flex; + align-items: center; + justify-content: center; +} + +.photo-more { + font-size: 32rpx; + font-weight: 600; + color: #fff; +} + +/* 底部占位 */ +.bottom-placeholder { + height: 180rpx; +} + +/* 底部操作按钮 */ +.action-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 220rpx; + background: linear-gradient(to top, #fff 60%, transparent); + display: flex; + align-items: center; + justify-content: center; + gap: 80rpx; + padding-bottom: env(safe-area-inset-bottom); +} + +.action-btn { + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.12); + transition: transform 0.2s ease; +} + +.action-btn:active { + transform: scale(0.95); +} + +/* X按钮 - 不喜欢 */ +.action-btn.dislike-btn { + width: 140rpx; + height: 140rpx; + background: #fff; + border: 3rpx solid #f3f4f6; +} + +.action-btn.dislike-btn .action-btn-icon { + width: 64rpx; + height: 64rpx; + opacity: 0.6; +} + +/* 对话按钮 - 微信绿色系 */ +.action-btn.chat-btn { + width: 140rpx; + height: 140rpx; + background: #07C160; + box-shadow: 0 0 0 6rpx rgba(7, 193, 96, 0.15), 0 12rpx 32rpx rgba(7, 193, 96, 0.35); +} + +.action-btn.chat-btn .action-btn-icon { + width: 64rpx; + height: 64rpx; + filter: brightness(0) invert(1); +} + + +/* ==================== 爱心弹窗样式 ==================== */ + +/* 弹窗遮罩 */ +.heart-popup-mask { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1001; + display: flex; + align-items: center; + justify-content: center; +} + +/* 弹窗主体 */ +.heart-popup { + width: 620rpx; + background: #F8F9FC; + border-radius: 48rpx; + padding: 0; + position: relative; + animation: heartPopupIn 0.3s ease-out; + box-shadow: 0 50rpx 100rpx -24rpx rgba(0, 0, 0, 0.25); + min-height: 720rpx; +} + +@keyframes heartPopupIn { + from { + opacity: 0; + transform: scale(0.85); + } + to { + opacity: 1; + transform: scale(1); + } +} + +/* 关闭按钮 */ +.heart-popup-close { + position: absolute; + top: 32rpx; + right: 32rpx; + width: 56rpx; + height: 56rpx; + display: flex; + justify-content: center; + align-items: center; + z-index: 10; +} + +.heart-popup-close image { + width: 32rpx; + height: 32rpx; + opacity: 0.7; +} + +/* 弹窗头部 - 白色圆角区域 */ +.heart-popup-header { + background: #FFFFFF; + border-radius: 0 0 60rpx 60rpx; + padding: 64rpx 40rpx 56rpx; + display: flex; + flex-direction: column; + align-items: center; + box-shadow: 0 2rpx 4rpx -2rpx rgba(0, 0, 0, 0.1), 0 2rpx 6rpx 0 rgba(0, 0, 0, 0.1); + position: relative; +} + +.popup-avatar-wrap { + width: 192rpx; + height: 192rpx; + border-radius: 50%; + overflow: hidden; + border: 4rpx solid #FFFFFF; + box-shadow: 0 8rpx 12rpx -8rpx rgba(0, 0, 0, 0.1), 0 20rpx 30rpx -6rpx rgba(0, 0, 0, 0.1); + margin-bottom: 40rpx; + position: relative; +} + +.popup-avatar { + width: 100%; + height: 100%; +} + +/* 头像右下角徽章 */ +.popup-avatar-badge { + position: absolute; + bottom: 0; + right: 0; + width: 56rpx; + height: 56rpx; + background: #FFFFFF; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4rpx 8rpx -4rpx rgba(0, 0, 0, 0.1), 0 8rpx 12rpx -2rpx rgba(0, 0, 0, 0.1); +} + +.popup-avatar-badge image { + width: 32rpx; + height: 32rpx; +} + +.popup-character-name { + font-size: 48rpx; + font-weight: 900; + color: #101828; + text-align: center; + line-height: 1.25; + letter-spacing: -0.025em; +} + +.popup-character-name .highlight { + color: #914584; +} + +/* 选项区域 */ +.heart-popup-options { + padding: 56rpx 40rpx 64rpx; + display: flex; + flex-direction: column; + gap: 32rpx; +} + +/* 选项卡片 */ +.heart-option-card { + background: #FFFFFF; + border: 2rpx solid #F3F4F6; + border-radius: 32rpx; + padding: 0 32rpx; + height: 150rpx; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10rpx; + box-shadow: 0 2rpx 4rpx -2rpx rgba(0, 0, 0, 0.1), 0 2rpx 6rpx 0 rgba(0, 0, 0, 0.1); + transition: all 0.2s ease; +} + +.heart-option-card:active { + transform: scale(0.98); +} + +/* 分享选项 - 粉色背景 */ +.heart-option-card.share-option { + background: #FFF0F5; + border-color: #FCE7F3; +} + +/* 选项左侧内容 */ +.heart-option-left { + display: flex; + align-items: center; + gap: 28rpx; + flex: 1; +} + +.heart-option-icon { + width: 96rpx; + height: 96rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + box-shadow: 0 2rpx 4rpx -2rpx rgba(0, 0, 0, 0.1), 0 2rpx 6rpx 0 rgba(0, 0, 0, 0.1); +} + +/* 分享图标 - 粉色 */ +.heart-option-icon.share-icon { + background: #FFF0F5; +} + +.heart-option-icon.share-icon image { + width: 52rpx; + height: 52rpx; +} + +/* 爱心图标 - 红色 */ +.heart-option-icon.heart-icon { + background: #FFF0F5; +} + +.heart-option-icon.heart-icon image { + width: 52rpx; + height: 52rpx; +} + +.heart-option-info { + display: flex; + flex-direction: column; + gap: 8rpx; + flex: 1; + min-width: 0; +} + +.heart-option-title { + font-size: 36rpx; + font-weight: 900; + line-height: 1.4; + letter-spacing: -0.025em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 分享选项标题 - 紫色 */ +.share-option .heart-option-title { + color: #914584; +} + +/* 爱心选项标题 - 黑色 */ +.heart-option .heart-option-title { + color: #101828; +} + +.heart-option-desc { + font-size: 28rpx; + font-weight: 700; + line-height: 1.5; + opacity: 0.8; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 分享选项描述 - 紫色 */ +.share-option .heart-option-desc { + color: #914584; +} + +/* 爱心选项描述 - 灰色 */ +.heart-option .heart-option-desc { + color: #6A7282; +} + +/* 选项右侧按钮 */ +.heart-option-btn { + width: 174rpx; + height: 100rpx; + border-radius: 100rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 40rpx; + font-weight: 900; + letter-spacing: -0.025em; + line-height: 1.5; + flex-shrink: 0; + box-shadow: 0 4rpx 8rpx -4rpx rgba(0, 0, 0, 0.1), 0 8rpx 12rpx -2rpx rgba(0, 0, 0, 0.1); +} + +/* 分享按钮 - 紫色 */ +.heart-option-btn.share-btn { + background: #914584; + color: #FFFFFF; +} + +/* 兑换按钮 - 灰色 */ +.heart-option-btn.exchange-btn { + background: #F3F4F6; + color: #4A5565; +} + +/* 暂不需要按钮 */ +.heart-popup-footer { + padding: 0 40rpx 56rpx; +} + +.heart-cancel-btn { + width: 100%; + height: 102rpx; + background: transparent; + border: none; + display: flex; + align-items: center; + justify-content: center; + font-size: 36rpx; + font-weight: 700; + color: #99A1AF; + letter-spacing: 0.025em; + line-height: 1.5; +} + +.heart-cancel-btn::after { + border: none; +} diff --git a/pages/chat-detail/chat-detail.js b/pages/chat-detail/chat-detail.js new file mode 100644 index 0000000..81f492c --- /dev/null +++ b/pages/chat-detail/chat-detail.js @@ -0,0 +1,2095 @@ +// pages/chat-detail/chat-detail.js +// 聊天详情页面 - 与AI角色聊天 + +const app = getApp() +const api = require('../../utils/api') +const util = require('../../utils/util') +const proactiveMessage = require('../../utils/proactiveMessage') +const imageUrl = require('../../utils/imageUrl') +const config = require('../../config/index') + +// 常用表情 +const EMOJIS = [ + "😊", "😀", "😁", "😃", "😂", "🤣", "😅", "😆", "😉", "😋", "😎", "😍", "😘", "🥰", "😗", "😙", + "🙂", "🤗", "🤩", "🤔", "😐", "😑", "😶", "🙄", "😏", "😣", "😥", "😮", "😯", "😪", "😫", "😴", + "😌", "😛", "😜", "😝", "😒", "😓", "😔", "😕", "🙃", "😲", "😖", "😞", "😟", "😤", "😢", "😭", + "😨", "😩", "😬", "😰", "😱", "😳", "😵", "😡", "😠", "😷", "🤒", "🤕", "😇", "🥳", "🥺", + "👋", "👌", "✌️", "🤞", "👍", "👎", "👏", "🙌", "🤝", "🙏", "💪", "❤️", "🧡", "💛", "💚", "💙", + "💜", "🖤", "💔", "💕", "💖", "💗", "💘", "💝", "🌹", "🌺", "🌻", "🌼", "🌷", "🎉", "🎊", "🎁" +] + +Page({ + data: { + statusBarHeight: 44, + navHeight: 96, + + // 角色信息 + characterId: '', + conversationId: '', + character: { + id: '', + name: '加载中...', + avatar: '', + isOnline: true, + job: '', + location: '', + gender: '', + age: '', + education: '', + serviceCount: 0, + returnCount: 0, + rating: '4.9', + motto: '', + qualification: '', + skills: [], + introduction: '' + }, + + // 用户头像 - 从用户信息获取,使用默认头像 + myAvatar: '/images/default-avatar.svg', + + // 消息列表 + messages: [], + + // 输入状态 + inputText: '', + inputFocus: false, + isVoiceMode: false, + isRecording: false, + showEmoji: false, + recordingDuration: 0, + voiceCancelHint: false, + + // AI状态 + isTyping: false, + isSending: false, + playingVoiceId: null, + + // 滚动控制 + scrollIntoView: '', + scrollTop: 0, // 当前滚动位置 + + // 加载状态 + loading: true, + loadingMore: false, + hasMore: true, + page: 1, + pageSize: 20, // 每页加载20条消息 + isFirstLoad: true, // 是否首次加载 + + // 人物介绍弹窗 + showProfilePopup: false, + + // 查看评价弹窗 + showReviewPopup: false, + reviews: [], + + // 更多功能面板 + showMorePanel: false, + + // 常用语列表 + quickReplies: [ + '你好,很高兴认识你~', + '最近怎么样?', + '有什么想聊的吗?', + '今天心情如何?', + '晚安,好梦~', + '早安,新的一天开始了!' + ], + showQuickReplyPopup: false, + + // 表情列表 + emojis: EMOJIS, + + // 约时间弹窗 + showSchedulePopup: false, + scheduleDate: '', + scheduleTime: '', + + // 礼物相关 + showGiftPopup: false, + giftList: [], + selectedGift: null, + userFlowers: 0, + + // 聊天配额相关 + remainingCount: 10, // 剩余免费次数(已废弃,保留兼容) + maxCount: 10, // 每日最大免费次数(已废弃,保留兼容) + isUnlocked: false, // 是否已解锁该角色 + isVip: false, // 是否为VIP用户 + showUnlockPopup: false, // 显示解锁弹窗 + todayCharacterId: '', // 今天已聊天的角色ID + heartCount: 0, // 用户爱心余额 + unlockHeartsCost: 500, // 默认解锁爱心成本 + + // 免费畅聊相关 + freeTime: null, + countdownText: '' + }, + + onLoad(options) { + const { statusBarHeight, navHeight } = app.globalData + + // 初始化消息处理相关变量 + this.pendingMessages = [] + this.messageTimer = null + this.isProcessing = false + + // 获取参数 + const characterId = options.id || '' + const conversationId = options.conversationId || '' + const characterName = decodeURIComponent(options.name || '') + + // 设置用户头像 + const userInfo = app.globalData.userInfo + const myAvatar = imageUrl.getAvatarUrl(userInfo?.avatar) + + this.setData({ + statusBarHeight, + navHeight, + characterId, + conversationId, + myAvatar, + 'character.name': characterName || '加载中...' + }) + + // 进入聊天详情页时,调用标记已读接口清除未读数 + if (conversationId) { + this.markConversationAsRead(conversationId) + } + + // 加载角色信息和聊天历史 + this.initChat() + }, + + onShow() { + // 每次显示页面时,刷新一次配额状态,确保免费畅聊时间等状态是最新的 + if (!this.data.loading) { + this.loadQuotaStatus() + } + }, + + onUnload() { + // 页面卸载时清理 + // 清除消息处理定时器 + if (this.messageTimer) { + clearTimeout(this.messageTimer) + this.messageTimer = null + } + // 清除图片回复定时器 + if (this.imageReplyTimers && this.imageReplyTimers.length > 0) { + this.imageReplyTimers.forEach(timer => clearTimeout(timer)) + this.imageReplyTimers = [] + } + // 清空待处理消息队列 + this.pendingMessages = [] + this.isProcessing = false + + // 离开页面时标记该角色的主动推送消息为已读 + if (this.data.characterId) { + proactiveMessage.markAsRead(this.data.characterId) + } + }, + + /** + * 标记会话已读 + * 进入聊天详情页时调用,清除未读数 + * @param {string} conversationId - 会话ID + */ + async markConversationAsRead(conversationId) { + if (!conversationId) { + console.log('[chat-detail] 没有conversationId,跳过标记已读') + return + } + + console.log('[chat-detail] 开始调用标记已读接口,conversationId:', conversationId) + + try { + const res = await api.chat.markAsRead(conversationId) + console.log('[chat-detail] 标记已读API响应:', JSON.stringify(res)) + + if (res.code === 0 || res.success) { + console.log('[chat-detail] 标记已读成功') + } else { + console.log('[chat-detail] 标记已读失败,响应:', res.message || res.error) + } + } catch (err) { + console.error('[chat-detail] 标记已读请求异常:', err) + } + }, + + /** + * 初始化聊天 + */ + async initChat() { + this.setData({ loading: true }) + + try { + // 检查登录状态和Token + const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) + const userId = wx.getStorageSync(config.STORAGE_KEYS.USER_ID) + console.log('[chat-detail] ========== 初始化聊天 ==========') + console.log('[chat-detail] Token存在:', !!token) + console.log('[chat-detail] Token长度:', token ? token.length : 0) + console.log('[chat-detail] UserId:', userId) + console.log('[chat-detail] CharacterId:', this.data.characterId) + + // 先加载配额状态 + await this.loadQuotaStatus() + + // 并行加载角色信息和聊天历史 + const [characterRes, historyRes] = await Promise.all([ + this.loadCharacterInfo(), + this.loadChatHistory() + ]) + + this.setData({ loading: false }) + + // 如果没有会话ID,创建新会话 + if (!this.data.conversationId && this.data.characterId) { + await this.createConversation() + } + } catch (err) { + console.error('初始化聊天失败', err) + this.setData({ loading: false }) + util.showError('加载失败') + } + }, + + /** + * 加载聊天配额状态 + */ + async loadQuotaStatus() { + const { characterId } = this.data + + if (!characterId) { + console.log('[chat-detail] 没有角色ID,使用默认配额') + return + } + + try { + const res = await api.chat.getQuota(characterId) + console.log('[chat-detail] 配额API响应:', JSON.stringify(res)) + + if (res && res.success && res.data) { + const quota = res.data + const isUnlocked = quota.is_unlocked || false + const isVip = quota.isVip || (quota.free_chat_time && quota.free_chat_time.isVip) || false + const freeChatTime = quota.free_chat_time || null + + const canChatByVip = !!isVip + const canChatByFreeTime = !!(freeChatTime && freeChatTime.isActive) + const canChat = isUnlocked || canChatByVip || canChatByFreeTime + + console.log('[chat-detail] 解析权限状态:', { isUnlocked, isVip, canChat, canChatByFreeTime }) + + this.setData({ + remainingCount: -1, + maxCount: 0, + isUnlocked: !!isUnlocked, + isVip: !!isVip, + todayCharacterId: quota.today_character_id || '', + freeTime: freeChatTime, + unlockHeartsCost: quota.unlock_config?.hearts_cost || 500 + }) + + // 处理免费畅聊倒计时 + if (freeChatTime && freeChatTime.isActive && freeChatTime.remainingSeconds > 0) { + this.startCountdown(freeChatTime.remainingSeconds) + } else { + this.stopCountdown() + } + + // 如果不能聊天且未解锁,显示解锁弹窗 + if (!canChat && !isUnlocked) { + console.log('[chat-detail] 无聊天权限,显示解锁弹窗') + this.setData({ showUnlockPopup: true }) + } else { + console.log('[chat-detail] 拥有聊天权限,确保解锁弹窗关闭') + this.setData({ showUnlockPopup: false }) + } + } + } catch (err) { + console.log('[chat-detail] 加载权限状态失败', err) + this.setData({ + remainingCount: -1, + maxCount: 0, + isUnlocked: false + }) + } + + // 同时加载用户爱心值 + await this.loadHeartBalance() + }, + + /** + * 开始倒计时 + */ + startCountdown(seconds) { + this.stopCountdown() + + let remaining = seconds + this.setData({ + countdownText: this.formatSeconds(remaining) + }) + + this.countdownTimer = setInterval(() => { + remaining-- + if (remaining <= 0) { + this.stopCountdown() + this.setData({ + 'freeTime.isActive': false, + 'freeTime.remainingSeconds': 0 + }) + } else { + this.setData({ + countdownText: this.formatSeconds(remaining) + }) + } + }, 1000) + }, + + /** + * 停止倒计时 + */ + stopCountdown() { + if (this.countdownTimer) { + clearInterval(this.countdownTimer) + this.countdownTimer = null + } + this.setData({ countdownText: '' }) + }, + + /** + * 格式化秒数为 MM:SS + */ + formatSeconds(s) { + const m = Math.floor(s / 60) + const rs = s % 60 + return `${m}:${rs < 10 ? '0' : ''}${rs}` + }, + + /** + * 加载用户爱心值 + * 使用 /api/auth/me 接口,该接口从 im_users.grass_balance 读取余额 + */ + async loadHeartBalance() { + try { + const res = await api.auth.getCurrentUser() + if (res.success && res.data) { + this.setData({ + heartCount: res.data.grass_balance || 0 + }) + console.log('[chat-detail] 爱心值加载成功:', res.data.grass_balance) + } + } catch (err) { + console.log('加载爱心值失败', err) + } + }, + + /** + * 加载角色信息 + */ + async loadCharacterInfo() { + if (!this.data.characterId) return + + try { + const res = await api.character.getDetail(this.data.characterId) + + if (res.success && res.data) { + // 处理头像URL - 后端已返回完整URL,前端只需兜底处理 + const avatarUrl = imageUrl.getCharacterAvatarUrl(res.data.avatar || res.data.logo) + + // 解析擅长领域 + let skills = res.data.hobbiesTags || res.data.traits || [] + if (!Array.isArray(skills) || skills.length === 0) { + skills = ['情感困惑', '职业压力', '成长创伤'] + } + + // 使用API返回的真实统计数据 + const serviceCount = res.data.serviceCount || 0 + const returnCount = res.data.returnCount || 0 + const avgRating = res.data.avgRating || 4.9 + + this.setData({ + character: { + id: res.data.id, + name: res.data.name, + avatar: avatarUrl, + isOnline: true, + voiceId: res.data.voice_id, + job: res.data.occupation || res.data.companionType || '心理咨询师', + location: res.data.location || res.data.province || '北京', + gender: res.data.gender === 'male' ? '男' : '女', + age: res.data.age || '90后', + education: '本科', + serviceCount: serviceCount, + returnCount: returnCount, + rating: avgRating.toFixed(2), + motto: res.data.openingLine || '每一次倾诉,都是心灵的释放', + qualification: '国家二级心理咨询师 | 情感咨询专家认证', + skills: skills.slice(0, 3), + introduction: res.data.selfIntroduction || res.data.about || '专业心理咨询培训' + } + }) + } + } catch (err) { + console.log('加载角色信息失败', err) + } + }, + + /** + * 加载聊天历史(首次加载最近20条) + */ + async loadChatHistory() { + const { characterId, pageSize } = this.data + + if (!characterId) { + const welcomeMsg = { + id: 'welcome', + text: `你好!我是${this.data.character.name},很高兴认识你~`, + isMe: false, + time: util.formatTime(new Date(), 'HH:mm'), + type: 'text' + } + this.setData({ + messages: [welcomeMsg], + isFirstLoad: false, + hasMore: false + }) + return + } + + try { + console.log('[chat-detail] 开始加载聊天历史, characterId:', characterId) + + // 首次只加载最近20条消息 + const res = await api.chat.getChatHistoryByCharacter(characterId, { + limit: pageSize, + page: 1 + }) + + console.log('[chat-detail] API响应:', JSON.stringify(res).substring(0, 200)) + + if (res.success && res.data && res.data.length > 0) { + console.log('[chat-detail] 收到历史消息数量:', res.data.length) + + const messages = res.data.map(msg => this.transformMessage(msg)) + + this.setData({ + messages, + hasMore: res.data.length >= pageSize, + page: 1, + isFirstLoad: false + }) + + console.log('[chat-detail] 消息已设置, 当前数量:', this.data.messages.length) + console.log('[chat-detail] 首次加载完成,不自动滚动到底部') + } else { + console.log('[chat-detail] 没有历史记录,显示欢迎消息') + const welcomeMsg = { + id: 'welcome', + text: `你好!我是${this.data.character.name},很高兴认识你~`, + isMe: false, + time: util.formatTime(new Date(), 'HH:mm'), + type: 'text' + } + this.setData({ + messages: [welcomeMsg], + isFirstLoad: false, + hasMore: false + }) + } + } catch (err) { + console.log('加载聊天历史失败:', err) + const welcomeMsg = { + id: 'welcome', + text: `你好!我是${this.data.character.name},很高兴认识你~`, + isMe: false, + time: util.formatTime(new Date(), 'HH:mm'), + type: 'text' + } + this.setData({ + messages: [welcomeMsg], + isFirstLoad: false, + hasMore: false + }) + } + }, + + /** + * 加载更多历史消息(向上翻页) + */ + async loadMoreHistory() { + const { characterId, loadingMore, hasMore, page, pageSize, messages } = this.data + + if (loadingMore || !hasMore || !characterId) { + return + } + + console.log('[chat-detail] 开始加载更多历史消息, page:', page + 1) + + this.setData({ loadingMore: true }) + + try { + const res = await api.chat.getChatHistoryByCharacter(characterId, { + limit: pageSize, + page: page + 1 + }) + + if (res.success && res.data && res.data.length > 0) { + console.log('[chat-detail] 加载到更多消息:', res.data.length, '条') + + const newMessages = res.data.map(msg => this.transformMessage(msg)) + + // 将新消息插入到列表开头(历史消息在前) + this.setData({ + messages: [...newMessages, ...messages], + hasMore: res.data.length >= pageSize, + page: page + 1, + loadingMore: false + }) + + console.log('[chat-detail] 历史消息加载完成,总消息数:', this.data.messages.length) + } else { + console.log('[chat-detail] 没有更多历史消息了') + this.setData({ + hasMore: false, + loadingMore: false + }) + } + } catch (err) { + console.error('[chat-detail] 加载更多历史消息失败:', err) + this.setData({ loadingMore: false }) + } + }, + + /** + * 滚动事件监听(检测是否滚动到顶部) + */ + onScroll(e) { + const { scrollTop } = e.detail + + // 滚动到顶部时加载更多历史消息 + if (scrollTop < 50 && !this.data.loadingMore && this.data.hasMore) { + console.log('[chat-detail] 滚动到顶部,触发加载更多') + this.loadMoreHistory() + } + }, + + /** + * 转换消息格式 + */ + transformMessage(msg) { + const baseMessage = { + id: msg.id, + text: msg.content, + isMe: msg.role === 'user', + time: util.formatTime(msg.created_at || msg.timestamp, 'HH:mm'), + type: msg.message_type || 'text' + } + + // 根据消息类型添加额外字段 + if (msg.message_type === 'image' && msg.image_url) { + baseMessage.imageUrl = msg.image_url + } else if (msg.message_type === 'voice' && msg.voice_url) { + baseMessage.audioUrl = msg.voice_url + baseMessage.duration = msg.voice_duration + } else if (msg.message_type === 'gift' && msg.gift_info) { + baseMessage.giftInfo = typeof msg.gift_info === 'string' ? JSON.parse(msg.gift_info) : msg.gift_info + } + + return baseMessage + }, + + /** + * 创建新会话 + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + async createConversation() { + try { + const res = await api.chat.createConversation(this.data.characterId) + + if (res.code === 0 && res.data) { + this.setData({ + conversationId: res.data.id + }) + } else if (res.success && res.data) { + this.setData({ + conversationId: res.data.id + }) + } + } catch (err) { + console.log('创建会话失败', err) + } + }, + + /** + * 返回上一页 + */ + onBack() { + wx.navigateBack() + }, + + /** + * 跳转到角色详情页 + */ + onGoToCharacterDetail() { + const { characterId } = this.data + if (characterId) { + wx.navigateTo({ + url: `/pages/character-detail/character-detail?id=${characterId}` + }) + } + }, + + /** + * 用户头像加载失败时的处理 + */ + onAvatarError() { + this.setData({ + myAvatar: '/images/default-avatar.svg' + }) + }, + + /** + * 更多操作 + */ + onMore() { + wx.showActionSheet({ + itemList: ['查看资料', '清空聊天记录', '举报'], + success: (res) => { + if (res.tapIndex === 0) { + // 查看资料 + wx.navigateTo({ + url: `/pages/character-detail/character-detail?id=${this.data.characterId}` + }) + } else if (res.tapIndex === 1) { + this.clearMessages() + } else if (res.tapIndex === 2) { + util.showSuccess('举报已提交') + } + } + }) + }, + + /** + * 清空消息 + * 只清空聊天记录,不删除会话 + * 会话仍然显示在消息列表中 + */ + async clearMessages() { + const confirmed = await util.showConfirm({ + title: '清空记录', + content: '确定要清空聊天记录吗?此操作不可恢复。' + }) + + if (!confirmed) return + + const { characterId } = this.data + + // 如果没有角色ID,只清空本地消息 + if (!characterId) { + this.setData({ messages: [] }) + util.showSuccess('已清空') + return + } + + wx.showLoading({ title: '清空中...' }) + + try { + // 调用后端API清空聊天记录(使用角色ID) + const res = await api.chat.clearChatHistory(characterId) + + if (res.success || res.code === 0) { + // 清空本地消息列表 + this.setData({ messages: [] }) + util.showSuccess('已清空') + } else { + throw new Error(res.message || '清空失败') + } + } catch (err) { + console.error('清空聊天记录失败:', err) + // 即使API失败,也清空本地消息 + this.setData({ messages: [] }) + util.showSuccess('已清空') + } finally { + wx.hideLoading() + } + }, + + /** + * 输入文字 + */ + onInput(e) { + this.setData({ inputText: e.detail.value }) + }, + + /** + * 发送消息 + */ + async onSend() { + const { inputText, characterId, conversationId, character, isUnlocked, isVip, freeTime, remainingCount } = this.data + + // 只检查输入是否为空 + if (!inputText.trim()) return + + // 检查登录 + if (app.checkNeedLogin()) return + + // 检查聊天权限 + // 1. 如果已解锁或VIP,直接放行 + // 2. 如果未解锁且非VIP,检查免费畅聊时间 + const canChatByFreeTime = !!(freeTime && freeTime.isActive) + const canChatByVip = !!isVip + + if (!isUnlocked && !canChatByVip && !canChatByFreeTime) { + console.log('[chat-detail] 无聊天权限,显示解锁弹窗', { isUnlocked, isVip, canChatByFreeTime }) + this.setData({ showUnlockPopup: true }) + return + } + + const messageText = inputText.trim() + const newId = util.generateId() + + // 添加用户消息到列表 + const userMessage = { + id: newId, + text: messageText, + isMe: true, + time: util.formatTime(new Date(), 'HH:mm'), + type: 'text' // 标记为文字消息 + } + + // 立即清空输入框,允许用户继续输入 + this.setData({ + messages: [...this.data.messages, userMessage], + inputText: '' + }, () => { + // 发送消息后立即滚动到底部 + this.scrollToBottom() + }) + + console.log('[chat-detail] 发送消息') + + // 将消息加入待处理队列 + this.pendingMessages.push(messageText) + + // 如果没有正在等待的定时器,启动延迟处理 + if (!this.messageTimer) { + this.startMessageTimer(characterId, conversationId, character, isUnlocked, remainingCount) + } + }, + + /** + * 待处理消息队列 + */ + pendingMessages: [], + messageTimer: null, + isProcessing: false, + + /** + * 启动消息处理定时器 + * 等待随机 2-8 秒,期间收集用户发送的所有消息 + */ + startMessageTimer(characterId, conversationId, character, isUnlocked, remainingCount) { + // 随机延迟 2-4 秒 + const randomDelay = Math.floor(Math.random() * 2000) + 2000 + + console.log('[chat-detail] 启动消息收集定时器,延迟:', randomDelay, 'ms') + + this.messageTimer = setTimeout(() => { + this.messageTimer = null + this.processPendingMessages(characterId, conversationId, character, isUnlocked, remainingCount) + }, randomDelay) + }, + + /** + * 处理待处理的消息队列 + * 将多条消息合并后发送给 AI + */ + async processPendingMessages(characterId, conversationId, character, isUnlocked, remainingCount) { + if (this.pendingMessages.length === 0) return + + // 如果正在处理中,延迟重试而不是直接丢弃消息 + if (this.isProcessing) { + console.log('[chat-detail] 消息处理中,延迟500ms后重试') + setTimeout(() => { + this.processPendingMessages(characterId, conversationId, character, isUnlocked, remainingCount) + }, 500) + return + } + + this.isProcessing = true + + // 取出所有待处理消息并清空队列 + const messagesToProcess = [...this.pendingMessages] + this.pendingMessages = [] + + // 合并多条消息为一条(用换行分隔) + const combinedMessage = messagesToProcess.join('\n') + + console.log('[chat-detail] 合并处理消息:', messagesToProcess.length, '条') + console.log('[chat-detail] 合并后内容:', combinedMessage) + + // 显示AI正在输入 + this.setData({ isTyping: true }) + + try { + // 构建对话历史(最近10条消息,只包含文字消息) + // 过滤掉图片消息,因为后端不需要处理图片内容 + const conversationHistory = this.data.messages + .slice(-10) + .filter(msg => msg.type !== 'image' && msg.text) // 只保留有文字内容的消息 + .map(msg => ({ + role: msg.isMe ? 'user' : 'assistant', + content: msg.text + })) + + // 发送合并后的消息到后端 + const res = await api.chat.sendMessage({ + character_id: characterId, + conversation_id: this.data.conversationId || conversationId, + message: combinedMessage, + conversationHistory: conversationHistory + }) + + this.setData({ isTyping: false }) + + // 检查是否需要解锁 + if (!res.success && (res.error === 'FREE_CHAT_TIME_EXPIRED' || res.error === 'FREE_CHAT_TIME_NOT_CLAIMED')) { + this.setData({ + showUnlockPopup: true, + 'freeTime.isActive': false + }) + + if (res.error === 'FREE_CHAT_TIME_NOT_CLAIMED') { + wx.showModal({ + title: '领取免费畅聊', + content: '领取100爱心值即可获得60分钟免费畅聊时间,是否现在去领取?', + confirmText: '去领取', + success: (modalRes) => { + if (modalRes.confirm) { + wx.switchTab({ url: '/pages/profile/profile' }) + } + } + }) + } + + this.isProcessing = false + return + } + + // 检查是否切换了角色 + if (!res.success && res.error === 'DIFFERENT_CHARACTER') { + this.setData({ showUnlockPopup: true }) + wx.showToast({ + title: res.message || '今天已与其他角色聊天', + icon: 'none' + }) + this.isProcessing = false + return + } + + if (res.success && res.data) { + // 更新会话ID(如果是新会话) + if (res.data.conversation_id && !this.data.conversationId) { + this.setData({ conversationId: res.data.conversation_id }) + } + + // 更新解锁状态(从后端返回的quota字段) + if (res.data.quota) { + const newIsUnlocked = res.data.quota.is_unlocked + + console.log('[chat-detail] 后端返回解锁状态:', { newIsUnlocked }) + + this.setData({ + isUnlocked: newIsUnlocked || this.data.isUnlocked + }) + } + + // 添加AI回复 + const aiMessage = { + id: res.data.id || util.generateId(), + text: res.data.content || res.data.message, + isMe: false, + time: util.formatTime(new Date(), 'HH:mm'), + audioUrl: res.data.audio_url, + type: 'text' // 标记为文字消息 + } + + this.setData({ + messages: [...this.data.messages, aiMessage] + }, () => { + // AI回复后滚动到底部 + this.scrollToBottom() + }) + } else { + throw new Error(res.error || res.message || '发送失败') + } + } catch (err) { + console.error('发送消息失败', err) + + // 开发模式下使用模拟AI回复 + const config = require('../../config/index') + if (config.DEBUG) { + console.log('[DEV] 使用模拟AI回复') + const mockResponse = this.getMockAIResponse(combinedMessage, character.name) + + const aiMessage = { + id: util.generateId(), + text: mockResponse, + isMe: false, + time: util.formatTime(new Date(), 'HH:mm'), + type: 'text' // 标记为文字消息 + } + + this.setData({ + messages: [...this.data.messages, aiMessage], + isTyping: false + }, () => { + this.scrollToBottom() + }) + + this.isProcessing = false + return + } + + this.setData({ isTyping: false }) + util.showError(err.message || '发送失败,请重试') + } + + this.isProcessing = false + }, + + /** + * 获取模拟AI回复(开发模式) + */ + getMockAIResponse(userMessage, characterName) { + const responses = [ + `嗯,我明白你的意思~`, + `这个问题很有趣呢,让我想想...`, + `谢谢你愿意和我分享这些~`, + `我觉得你说得很有道理!`, + `哈哈,你真的很有趣~`, + `我一直都在这里陪着你哦~`, + `能和你聊天真的很开心!`, + `你今天心情怎么样呀?`, + `我很喜欢和你聊天的感觉~`, + `嗯嗯,我在认真听你说呢~` + ] + + // 根据用户消息内容选择合适的回复 + if (userMessage.includes('你好') || userMessage.includes('嗨') || userMessage.includes('hi')) { + return `你好呀!我是${characterName},很高兴认识你~今天想聊点什么呢?` + } + if (userMessage.includes('名字') || userMessage.includes('叫什么')) { + return `我叫${characterName}呀,你可以这样叫我~` + } + if (userMessage.includes('喜欢')) { + return `我也很喜欢和你聊天呢!你喜欢什么呀?` + } + if (userMessage.includes('开心') || userMessage.includes('高兴')) { + return `看到你开心我也很开心呢!希望你每天都这么快乐~` + } + if (userMessage.includes('难过') || userMessage.includes('伤心') || userMessage.includes('不开心')) { + return `抱抱你~有什么不开心的事情可以和我说说,我会一直陪着你的。` + } + + // 随机选择一个回复 + const randomIndex = Math.floor(Math.random() * responses.length) + return responses[randomIndex] + }, + + /** + * 滚动到底部(仅在发送/接收新消息时调用) + * 使用 scroll-into-view 属性,自动滚动到最后一条消息 + */ + scrollToBottom() { + const messages = this.data.messages + if (messages && messages.length > 0) { + // 使用 setTimeout 确保 DOM 已更新 + setTimeout(() => { + this.setData({ + scrollIntoView: `msg-${messages.length - 1}` + }) + }, 100) + } + }, + + /** + * 滚动到顶部(加载更多历史消息后保持位置) + */ + scrollToTop() { + this.setData({ + scrollTop: 0 + }) + }, + + /** + * 切换语音模式 + */ + onVoiceMode() { + this.setData({ + isVoiceMode: !this.data.isVoiceMode, + showEmoji: false + }) + }, + + /** + * 开始录音 + */ + onVoiceStart() { + this.setData({ isRecording: true }) + + wx.showToast({ + title: '正在录音...', + icon: 'none', + duration: 60000 + }) + + // 开始录音 + const recorderManager = wx.getRecorderManager() + recorderManager.start({ + duration: 60000, + format: 'mp3' + }) + + this.recorderManager = recorderManager + }, + + /** + * 结束录音 + */ + onVoiceEnd() { + this.setData({ isRecording: false }) + wx.hideToast() + + if (this.recorderManager) { + this.recorderManager.stop() + + this.recorderManager.onStop((res) => { + // 发送语音消息(暂时转为文字) + const newId = util.generateId() + const voiceMessage = { + id: newId, + text: '[语音消息]', + isMe: true, + time: util.formatTime(new Date(), 'HH:mm'), + type: 'voice', + audioUrl: res.tempFilePath + } + + this.setData({ + messages: [...this.data.messages, voiceMessage] + }) + + this.scrollToBottom() + }) + } + }, + + /** + * 切换表情面板 + */ + onEmojiToggle() { + // 收起键盘 + this.setData({ + inputFocus: false + }) + + // 延迟一点再显示面板,确保键盘先收起 + setTimeout(() => { + this.setData({ + showEmoji: !this.data.showEmoji, + showMorePanel: false, + isVoiceMode: false + }) + }, 50) + }, + + /** + * 点击+号,显示更多功能面板 + */ + onAddMore() { + // 收起键盘 + this.setData({ + inputFocus: false + }) + + // 延迟一点再显示面板,确保键盘先收起 + setTimeout(() => { + this.setData({ + showMorePanel: !this.data.showMorePanel, + showEmoji: false, + isVoiceMode: false + }) + }, 50) + }, + + /** + * 关闭所有面板 + */ + onClosePanels() { + this.setData({ + showEmoji: false, + showMorePanel: false + }) + }, + + /** + * 点击聊天区域关闭面板 + */ + onTapChatArea() { + if (this.data.showEmoji || this.data.showMorePanel) { + this.setData({ + showEmoji: false, + showMorePanel: false + }) + } + }, + + /** + * 选择表情 + */ + onEmojiSelect(e) { + const emoji = e.currentTarget.dataset.emoji + this.setData({ + inputText: this.data.inputText + emoji + }) + }, + + /** + * 播放语音消息 + */ + onPlayVoice(e) { + const { id, url } = e.currentTarget.dataset + + if (!url) { + util.showError('语音不可用') + return + } + + const innerAudioContext = wx.createInnerAudioContext() + innerAudioContext.src = url + innerAudioContext.play() + + innerAudioContext.onEnded(() => { + innerAudioContext.destroy() + }) + }, + + /** + * 播放AI语音 + */ + async onPlayAIVoice(e) { + const { id, text } = e.currentTarget.dataset + const { character } = this.data + + if (!character.voiceId) { + util.showError('该角色暂不支持语音') + return + } + + util.showLoading('生成语音中...') + + try { + const res = await api.tts.synthesize({ + text: text, + voice_id: character.voiceId, + character_id: character.id + }) + + util.hideLoading() + + if (res.success && res.data && res.data.audio_url) { + const innerAudioContext = wx.createInnerAudioContext() + innerAudioContext.src = res.data.audio_url + innerAudioContext.play() + } else { + util.showError('语音生成失败') + } + } catch (err) { + util.hideLoading() + util.showError('语音生成失败') + } + }, + + /** + * 开始聊天(免费倾诉) + */ + onStartChat() { + // 聚焦输入框 + this.setData({ showEmoji: false }) + }, + + /** + * 显示人物介绍弹窗 + */ + onShowProfile() { + this.setData({ showProfilePopup: true }) + }, + + /** + * 关闭人物介绍弹窗 + */ + onCloseProfile() { + this.setData({ showProfilePopup: false }) + }, + + /** + * 查看评价 + */ + onShowReviews() { + // 生成模拟评价数据 + const mockReviews = [ + { + id: 1, + phone: '138****6172', + date: '2024-12-15 14:32', + content: '老师很有耐心,倾听我的问题后给出了很中肯的建议。咨询后感觉心里轻松了很多,对未来也有了新的规划...', + tags: ['专业', '耐心', '有效果'], + reply: '谢谢您的信任,很高兴能够帮助到您。希望您能继续保持积极的心态,有任何问题随时可以来找我交流。', + likes: 23 + }, + { + id: 2, + phone: '186****3298', + date: '2024-12-10 09:15', + content: '第一次尝试心理咨询,老师非常专业,让我感觉很放松。通过几次咨询,我对自己的情绪有了更好的认识...', + tags: ['专业', '温暖', '有帮助'], + reply: '能够陪伴您成长是我的荣幸,继续加油!', + likes: 15 + }, + { + id: 3, + phone: '159****7721', + date: '2024-12-05 20:48', + content: '老师的声音很温柔,聊天的过程中感觉很舒服。虽然问题还在,但是心态好了很多,会继续找老师咨询的...', + tags: ['温柔', '善于倾听'], + reply: '', + likes: 8 + }, + { + id: 4, + phone: '177****4532', + date: '2024-11-28 16:22', + content: '咨询师很专业,能够快速理解我的问题并给出建议。性价比很高,会推荐给朋友...', + tags: ['专业', '高效'], + reply: '', + likes: 12 + }, + { + id: 5, + phone: '133****8965', + date: '2024-11-20 11:05', + content: '非常好的一次体验,老师很有同理心,让我感受到了被理解和支持...', + tags: ['有同理心', '支持'], + reply: '感谢您的认可,祝您生活愉快!', + likes: 6 + } + ] + + this.setData({ + showReviewPopup: true, + reviews: mockReviews + }) + }, + + /** + * 关闭评价弹窗 + */ + onCloseReviews() { + this.setData({ showReviewPopup: false }) + }, + + /** + * 阻止弹窗滚动穿透 + */ + preventMove() { + return false + }, + + /** + * 关闭解锁弹窗 + */ + closeUnlockPopup() { + this.setData({ showUnlockPopup: false }) + }, + + /** + * 跳转到个人中心(去领取奖励) + */ + onGoToProfile() { + wx.switchTab({ + url: '/pages/profile/profile' + }) + }, + + /** + * 爱心兑换解锁 + */ + async onExchangeHearts() { + const { character, heartCount, unlockHeartsCost } = this.data + const config = require('../../config/index') + + // 检查登录 + const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) + if (!token) { + wx.showToast({ title: '请先登录', icon: 'none' }) + setTimeout(() => { + wx.navigateTo({ url: '/pages/login/login' }) + }, 1500) + return + } + + // 检查爱心值,不足时提示并跳转充值页面 + if (heartCount < unlockHeartsCost) { + wx.showToast({ title: '爱心值不足,去充值', icon: 'none' }) + setTimeout(() => { + this.setData({ showUnlockPopup: false }) + wx.navigateTo({ url: '/pages/recharge/recharge' }) + }, 1500) + return + } + + wx.showLoading({ title: '兑换中...' }) + + try { + const res = await api.character.unlock({ + character_id: character.id, + unlock_type: 'hearts' + }) + + wx.hideLoading() + + if (res.success || res.code === 0) { + wx.showToast({ title: '解锁成功', icon: 'success' }) + + // 更新状态,使用后端返回的剩余爱心数 + const remainingHearts = res.data?.remaining_hearts ?? (heartCount - unlockHeartsCost) + this.setData({ + heartCount: remainingHearts, + isUnlocked: true, + remainingCount: -1, // -1 表示无限 + showUnlockPopup: false + }) + } else { + wx.showToast({ title: res.message || '兑换失败', icon: 'none' }) + } + } catch (err) { + wx.hideLoading() + console.error('爱心兑换失败', err) + wx.showToast({ title: '网络错误,请重试', icon: 'none' }) + } + }, + + /** + * 直接购买解锁(9.9元) + * 使用 /api/payment/unified-order 接口 + * 测试模式下返回 testMode: true,订单直接完成,无需调用微信支付 + */ + async onPurchaseDirect() { + const { character } = this.data + const config = require('../../config/index') + + // 检查登录 + const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) + if (!token) { + wx.showToast({ title: '请先登录', icon: 'none' }) + setTimeout(() => { + wx.navigateTo({ url: '/pages/login/login' }) + }, 1500) + return + } + + wx.showLoading({ title: '创建订单中...' }) + + try { + // 调用统一支付订单接口 + const res = await api.payment.createUnifiedOrder({ + type: 'character_unlock', + character_id: character.id, + amount: 9.9 + }) + + wx.hideLoading() + + console.log('[chat-detail onPurchaseDirect] API返回:', JSON.stringify(res)) + + if (res.success) { + // 检查是否为测试模式(兼容多种判断方式) + const isTestMode = res.testMode || res.test_mode || res.data?.testMode || res.data?.test_mode || + // 如果 payParams.package 包含 mock_prepay,也认为是测试模式 + (res.payParams?.package && res.payParams.package.includes('mock_prepay')) + + if (isTestMode) { + // 测试模式:订单已直接完成,无需调用微信支付 + wx.showToast({ title: '购买成功', icon: 'success' }) + + // 重新加载配额状态 + await this.loadQuotaStatus() + + // 更新状态 + this.setData({ + isUnlocked: true, + remainingCount: -1, + showUnlockPopup: false + }) + } else { + // 正式模式:调用微信支付 + const payParams = res.payParams || res.pay_params || res.data?.payParams || res.data?.pay_params + + if (!payParams || !payParams.timeStamp) { + wx.showToast({ title: '支付参数错误', icon: 'none' }) + return + } + + wx.requestPayment({ + timeStamp: payParams.timeStamp, + nonceStr: payParams.nonceStr, + package: payParams.package, + signType: payParams.signType || 'RSA', + paySign: payParams.paySign, + success: async () => { + wx.showToast({ title: '解锁成功', icon: 'success' }) + + // 重新加载配额状态 + await this.loadQuotaStatus() + + // 更新状态 + this.setData({ + isUnlocked: true, + remainingCount: -1, + showUnlockPopup: false + }) + }, + fail: (err) => { + if (err.errMsg !== 'requestPayment:fail cancel') { + wx.showToast({ title: '支付失败', icon: 'none' }) + } + } + }) + } + } else { + wx.showToast({ title: res.message || '创建订单失败', icon: 'none' }) + } + } catch (err) { + wx.hideLoading() + console.error('购买解锁失败', err) + wx.showToast({ title: '网络错误,请重试', icon: 'none' }) + } + }, + + /** + * 拍照 + */ + onTakePhoto() { + this.setData({ showMorePanel: false }) + + wx.chooseMedia({ + count: 1, + mediaType: ['image'], + sourceType: ['camera'], + camera: 'back', + success: (res) => { + const tempFilePath = res.tempFiles[0].tempFilePath + this.sendImageMessage(tempFilePath) + }, + fail: (err) => { + if (err.errMsg !== 'chooseMedia:fail cancel') { + util.showError('拍照失败') + } + } + }) + }, + + /** + * 从相册选择图片 + */ + onChooseImage() { + this.setData({ showMorePanel: false }) + + wx.chooseMedia({ + count: 9, + mediaType: ['image'], + sourceType: ['album'], + success: (res) => { + res.tempFiles.forEach(file => { + this.sendImageMessage(file.tempFilePath) + }) + }, + fail: (err) => { + if (err.errMsg !== 'chooseMedia:fail cancel') { + util.showError('选择图片失败') + } + } + }) + }, + + /** + * 发送图片消息 + * 发送图片后,先上传到服务器,然后保存到数据库,最后AI返回预设的图片回复话术 + */ + async sendImageMessage(tempFilePath) { + const newId = util.generateId() + + // 先添加本地消息(显示上传中状态) + const imageMessage = { + id: newId, + type: 'image', + imageUrl: tempFilePath, + isMe: true, + time: util.formatTime(new Date(), 'HH:mm'), + uploading: true // 标记为上传中 + } + + this.setData({ + messages: [...this.data.messages, imageMessage] + }, () => { + this.scrollToBottom() + }) + + try { + // 1. 上传图片到服务器 + console.log('[chat-detail] 开始上传图片:', tempFilePath) + + // 检查登录状态 + const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) + console.log('[chat-detail] Token存在:', !!token) + console.log('[chat-detail] Token长度:', token ? token.length : 0) + + const uploadRes = await api.uploadFile(tempFilePath, 'uploads') + + if (!uploadRes || !uploadRes.success || !uploadRes.data || !uploadRes.data.url) { + throw new Error('图片上传失败') + } + + const imageUrl = uploadRes.data.url + console.log('[chat-detail] 图片上传成功:', imageUrl) + + // 2. 更新本地消息,移除上传中状态 + const messages = this.data.messages.map(msg => { + if (msg.id === newId) { + return { ...msg, imageUrl: imageUrl, uploading: false } + } + return msg + }) + this.setData({ messages }) + + // 3. 保存图片消息到数据库 + try { + await api.chat.sendImage({ + character_id: this.data.characterId, + conversation_id: this.data.conversationId, + image_url: imageUrl + }) + console.log('[chat-detail] 图片消息已保存到数据库') + } catch (err) { + console.error('[chat-detail] 保存图片消息失败:', err) + // 保存失败不影响显示,继续执行 + } + + // 4. 使用独立的图片回复定时器,避免与文字消息冲突 + // 随机延迟 2-4 秒后显示AI回复 + const randomDelay = Math.floor(Math.random() * 2000) + 2000 + + // 存储定时器ID,用于页面卸载时清理 + const imageReplyTimer = setTimeout(async () => { + // 检查页面是否还存在 + if (!this.data) return + + // 标记图片回复正在处理中 + this.isImageReplyProcessing = true + + // 只有在没有正在输入状态时才显示(避免覆盖文字消息的输入状态) + const shouldShowTyping = !this.data.isTyping + if (shouldShowTyping) { + this.setData({ isTyping: true }) + } + + try { + // 调用图片回复话术API + const res = await api.imageReply.getRandom() + + // 检查页面是否还存在 + if (!this.data) { + this.isImageReplyProcessing = false + return + } + + // 只有当前是图片回复触发的isTyping时才关闭 + if (shouldShowTyping && !this.isProcessing) { + this.setData({ isTyping: false }) + } + + if (res && res.success && res.data && res.data.content) { + // 添加AI回复 + const aiMessage = { + id: util.generateId(), + text: res.data.content, + isMe: false, + time: util.formatTime(new Date(), 'HH:mm'), + type: 'text' // 标记为文字消息 + } + + this.setData({ + messages: [...this.data.messages, aiMessage] + }, () => { + this.scrollToBottom() + }) + } else { + // API返回失败,使用默认回复 + this.showDefaultImageReply() + } + } catch (err) { + console.error('[chat-detail] 获取图片回复话术失败:', err) + if (this.data) { + // 只有当前是图片回复触发的isTyping时才关闭 + if (shouldShowTyping && !this.isProcessing) { + this.setData({ isTyping: false }) + } + // 使用默认回复 + this.showDefaultImageReply() + } + } finally { + // 清除图片回复处理标记 + this.isImageReplyProcessing = false + } + }, randomDelay) + + // 保存定时器引用,用于清理 + if (!this.imageReplyTimers) { + this.imageReplyTimers = [] + } + this.imageReplyTimers.push(imageReplyTimer) + + } catch (err) { + console.error('[chat-detail] 图片上传失败:', err) + + // 更新消息状态为失败 + const messages = this.data.messages.map(msg => { + if (msg.id === newId) { + return { ...msg, uploading: false, uploadFailed: true } + } + return msg + }) + this.setData({ messages }) + + util.showError('图片发送失败') + } + }, + + /** + * 显示默认图片回复(API失败时的兜底) + */ + showDefaultImageReply() { + const defaultReplies = [ + '哇,这张图片真好看!', + '谢谢你分享这张图片给我~', + '这张图片很有意思呢!', + '收到你的图片啦,真棒!' + ] + const randomIndex = Math.floor(Math.random() * defaultReplies.length) + + const aiMessage = { + id: util.generateId(), + text: defaultReplies[randomIndex], + isMe: false, + time: util.formatTime(new Date(), 'HH:mm'), + type: 'text' // 标记为文字消息 + } + + this.setData({ + messages: [...this.data.messages, aiMessage] + }, () => { + this.scrollToBottom() + }) + }, + + /** + * 预览图片 + */ + onPreviewImage(e) { + const url = e.currentTarget.dataset.url + const urls = this.data.messages + .filter(msg => msg.type === 'image') + .map(msg => msg.imageUrl) + + wx.previewImage({ + current: url, + urls: urls + }) + }, + + /** + * 发送礼物 + */ + onSendGift() { + this.setData({ showMorePanel: false }) + + // 加载礼物列表 + this.loadGiftList() + this.setData({ showGiftPopup: true }) + }, + + /** + * 加载礼物列表 + */ + async loadGiftList() { + // 模拟礼物数据 + const giftList = [ + { id: 1, name: '玫瑰花', price: 10, image: '/images/gift-rose.png' }, + { id: 2, name: '爱心', price: 20, image: '/images/gift-heart.png' }, + { id: 3, name: '蛋糕', price: 50, image: '/images/gift-cake.png' }, + { id: 4, name: '钻戒', price: 100, image: '/images/gift-ring.png' }, + { id: 5, name: '跑车', price: 500, image: '/images/gift-car.png' }, + { id: 6, name: '城堡', price: 1000, image: '/images/gift-castle.png' }, + { id: 7, name: '火箭', price: 2000, image: '/images/gift-rocket.png' }, + { id: 8, name: '皇冠', price: 5000, image: '/images/gift-crown.png' } + ] + + this.setData({ giftList }) + }, + + /** + * 选择礼物 + */ + onSelectGift(e) { + const gift = e.currentTarget.dataset.gift + this.setData({ selectedGift: gift }) + }, + + /** + * 关闭礼物弹窗 + */ + onCloseGiftPopup() { + this.setData({ + showGiftPopup: false, + selectedGift: null + }) + }, + + /** + * 确认发送礼物 + */ + async onConfirmSendGift() { + const { selectedGift, userFlowers, character } = this.data + + if (!selectedGift) { + util.showError('请选择礼物') + return + } + + if (userFlowers < selectedGift.price) { + util.showError('花朵余额不足') + // 跳转充值页面 + setTimeout(() => { + wx.navigateTo({ url: '/pages/recharge/recharge' }) + }, 1500) + return + } + + // 发送礼物消息 + const newId = util.generateId() + const giftMessage = { + id: newId, + type: 'gift', + text: `送出了 ${selectedGift.name}`, + giftInfo: selectedGift, + isMe: true, + time: util.formatTime(new Date(), 'HH:mm') + } + + this.setData({ + messages: [...this.data.messages, giftMessage], + showGiftPopup: false, + selectedGift: null, + userFlowers: userFlowers - selectedGift.price + }, () => { + this.scrollToBottom() + }) + + util.showSuccess('礼物已送出') + + // TODO: 调用后端API发送礼物 + }, + + /** + * 语音通话 + */ + onVoiceCall() { + this.setData({ showMorePanel: false }) + + wx.showModal({ + title: '语音通话', + content: '语音通话功能即将上线,敬请期待~', + showCancel: false, + confirmText: '知道了' + }) + }, + + /** + * 常用语 + */ + onQuickReply() { + this.setData({ showMorePanel: false }) + + wx.showActionSheet({ + itemList: this.data.quickReplies, + success: (res) => { + const selectedReply = this.data.quickReplies[res.tapIndex] + this.setData({ inputText: selectedReply }) + } + }) + }, + + /** + * 约时间 + */ + onScheduleTime() { + this.setData({ showMorePanel: false }) + + wx.showModal({ + title: '约时间', + content: '预约功能即将上线,敬请期待~', + showCancel: false, + confirmText: '知道了' + }) + }, + + /** + * 抢红包 + */ + onRedPacket() { + this.setData({ showMorePanel: false }) + + wx.showModal({ + title: '抢红包', + content: '红包功能即将上线,敬请期待~', + showCancel: false, + confirmText: '知道了' + }) + }, + + /** + * 测结果 + */ + onTestResult() { + this.setData({ showMorePanel: false }) + + // 跳转到测试结果页面 + wx.showModal({ + title: '测结果', + content: '心理测试功能即将上线,敬请期待~', + showCancel: false, + confirmText: '知道了' + }) + }, + + /** + * 语音录制相关方法 + */ + onVoiceTouchStart(e) { + this.touchStartY = e.touches[0].clientY + this.setData({ + isRecording: true, + voiceCancelHint: false, + recordingDuration: 0 + }) + + // 开始录音 + const recorderManager = wx.getRecorderManager() + recorderManager.start({ + duration: 60000, + format: 'mp3' + }) + + this.recorderManager = recorderManager + + // 录音时长计时 + this.recordingTimer = setInterval(() => { + this.setData({ + recordingDuration: this.data.recordingDuration + 1 + }) + }, 1000) + }, + + onVoiceTouchMove(e) { + const moveY = e.touches[0].clientY + const diff = this.touchStartY - moveY + + // 上滑超过50px显示取消提示 + this.setData({ + voiceCancelHint: diff > 50 + }) + }, + + onVoiceTouchEnd() { + clearInterval(this.recordingTimer) + + const { voiceCancelHint, recordingDuration, characterId, character, isUnlocked, remainingCount } = this.data + + this.setData({ isRecording: false }) + + if (this.recorderManager) { + this.recorderManager.stop() + + if (voiceCancelHint) { + // 取消发送 + util.showToast('已取消') + return + } + + if (recordingDuration < 1) { + util.showError('录音时间太短') + return + } + + this.recorderManager.onStop(async (res) => { + console.log('[chat-detail] 录音完成:', res.tempFilePath, '时长:', recordingDuration) + + // 先显示语音消息(带识别中状态) + const newId = util.generateId() + const voiceMessage = { + id: newId, + type: 'voice', + audioUrl: res.tempFilePath, + duration: recordingDuration, + isMe: true, + time: util.formatTime(new Date(), 'HH:mm'), + recognizing: true, // 识别中状态 + recognizedText: '' // 识别出的文字 + } + + this.setData({ + messages: [...this.data.messages, voiceMessage] + }, () => { + this.scrollToBottom() + }) + + // 进行语音识别 + try { + wx.showLoading({ title: '语音识别中...' }) + + // 读取音频文件并转换为base64 + const fs = wx.getFileSystemManager() + const audioData = fs.readFileSync(res.tempFilePath) + const audioBase64 = wx.arrayBufferToBase64(audioData) + + // 调用语音识别API + const recognizeRes = await api.speech.recognize({ + audio: audioBase64, + format: 'mp3' + }) + + wx.hideLoading() + + let recognizedText = '' + if (recognizeRes.success && recognizeRes.data && recognizeRes.data.text) { + recognizedText = recognizeRes.data.text + console.log('[chat-detail] 语音识别结果:', recognizedText) + } else { + // 识别失败,使用默认文字 + recognizedText = '[语音消息]' + console.log('[chat-detail] 语音识别失败,使用默认文字') + } + + // 更新语音消息的识别状态 + const messages = this.data.messages.map(msg => { + if (msg.id === newId) { + return { ...msg, recognizing: false, recognizedText } + } + return msg + }) + this.setData({ messages }) + + // 如果识别出了有效文字,发送给AI + if (recognizedText && recognizedText !== '[语音消息]') { + // 检查聊天权限 + const canChatByFreeTime = !!(this.data.freeTime && this.data.freeTime.isActive) + const canChatByVip = !!this.data.isVip + + if (!isUnlocked && !canChatByVip && !canChatByFreeTime) { + console.log('[chat-detail] 语音消息无聊天权限', { isUnlocked, isVip, canChatByFreeTime }) + this.setData({ showUnlockPopup: true }) + return + } + + // 将识别出的文字加入待处理队列 + this.pendingMessages.push(recognizedText) + + // 如果没有正在等待的定时器,启动延迟处理 + if (!this.messageTimer) { + this.startMessageTimer(characterId, this.data.conversationId, character, isUnlocked, remainingCount) + } + } + + } catch (err) { + wx.hideLoading() + console.error('[chat-detail] 语音识别失败:', err) + + // 更新消息状态 + const messages = this.data.messages.map(msg => { + if (msg.id === newId) { + return { ...msg, recognizing: false, recognizedText: '[语音消息]' } + } + return msg + }) + this.setData({ messages }) + + util.showError('语音识别失败') + } + }) + } + }, + + onVoiceTouchCancel() { + clearInterval(this.recordingTimer) + this.setData({ isRecording: false }) + + if (this.recorderManager) { + this.recorderManager.stop() + } + }, + + /** + * 消息长按操作 + */ + onMessageLongPress(e) { + const item = e.currentTarget.dataset.item + + wx.showActionSheet({ + itemList: ['复制', '删除'], + success: (res) => { + if (res.tapIndex === 0) { + // 复制 + wx.setClipboardData({ + data: item.text, + success: () => { + util.showSuccess('已复制') + } + }) + } else if (res.tapIndex === 1) { + // 删除 + const messages = this.data.messages.filter(msg => msg.id !== item.id) + this.setData({ messages }) + } + } + }) + }, + + /** + * 阻止事件冒泡 + */ + preventBubble() { + return + }, + + /** + * 阻止触摸穿透 + */ + preventTouchMove() { + return false + } +}) diff --git a/pages/chat-detail/chat-detail.json b/pages/chat-detail/chat-detail.json new file mode 100644 index 0000000..b16ef3e --- /dev/null +++ b/pages/chat-detail/chat-detail.json @@ -0,0 +1,4 @@ +{ + "navigationStyle": "custom", + "usingComponents": {} +} diff --git a/pages/chat-detail/chat-detail.wxml b/pages/chat-detail/chat-detail.wxml new file mode 100644 index 0000000..cd399a1 --- /dev/null +++ b/pages/chat-detail/chat-detail.wxml @@ -0,0 +1,406 @@ + + + + + + + + + + + + + + 返回 + + + + + + + + {{character.name[0] || 'AI'}} + + + {{character.name}} + + + + + + + + + + + + + + + + 加载中... + + 向上滑动加载更多 + + + + 没有更多消息了 + + + + + 与 {{character.name}} 的加密对话 + + + + + + + + + + + {{character.name[0] || 'AI'}} + + + + + + {{item.text}} + + + + + + + + + + + + + {{item.duration || 1}}″ + + + {{item.time}} + + + + + + {{item.time}} + + + + + + + + + {{item.text}} + + + + + + + + {{item.duration || 1}}″ + + + + + + + + + 识别中... + {{item.recognizedText}} + + + + + + {{item.text}} + + + {{item.time}} + + + + + + + + + + + + + + + + {{character.name[0] || 'AI'}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{isRecording ? (voiceCancelHint ? '松开 取消' : '松开 发送') : '按住 说话'}} + + + + + + + + + + + + + + + + + + + + + + + + + + + {{item}} + + + + + + + + + + + + + + + + 照片 + + + + + + + 拍摄 + + + + + + + 礼物 + + + + + + + + + + + + + + + + + + + + {{voiceCancelHint ? '松开手指,取消发送' : '手指上划,取消发送'}} + {{recordingDuration}}″ + + + + + + + + 选择礼物 + + + + + + + + + {{item.name}} + + + {{item.price}} + + + + + + + 我的花朵: + + {{userFlowers || 0}} + + + 赠送 + + + + + + + + + + + × + + + + + + + + + + + + + + + + + + 解锁与 + {{character.name}} + 的专属聊天 + + + + + + + + + + + + + {{unlockHeartsCost}} 爱心 + {{heartCount >= unlockHeartsCost ? '爱心值充足 立即兑换' : '爱心值不足 去充值'}} + + + + 兑换 + + + + + + + + ¥ + + + 9.9元 + 限时特惠 立即购买 + + + + 购买 + + + + + + 暂不需要 + + + + + diff --git a/pages/chat-detail/chat-detail.wxss b/pages/chat-detail/chat-detail.wxss new file mode 100644 index 0000000..51cb82c --- /dev/null +++ b/pages/chat-detail/chat-detail.wxss @@ -0,0 +1,1659 @@ +/* AI智能体聊天详情页样式 - 基于Figma设计 */ + +/* 页面容器 */ +.page-container { + min-height: 100vh; + background: #F5F2FD; + display: flex; + flex-direction: column; + position: relative; +} + +/* 聊天区域包装器 - 使用固定定位确保正确布局 */ +.chat-area-wrapper { + position: fixed; + left: 0; + right: 0; + bottom: 120rpx; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* 面板打开时的透明遮罩层 */ +.panel-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: transparent; + z-index: 98; +} + +/* 状态栏区域 */ +.status-bar-area { + position: fixed; + top: 0; + left: 0; + right: 0; + background: rgba(242, 237, 255, 0.6); + z-index: 101; +} + +/* 顶部导航栏 */ +.nav-header { + position: fixed; + left: 0; + right: 0; + background: rgba(255, 255, 255, 0.95); + border-bottom: 2rpx solid #F3F4F6; + z-index: 100; +} + +.nav-content { + display: flex; + align-items: center; + justify-content: space-between; + height: 98rpx; + padding: 0 16rpx; +} + +/* 返回按钮 */ +.nav-back { + display: flex; + align-items: center; + gap: 4rpx; + padding: 16rpx; + min-width: 160rpx; +} + +.back-icon { + width: 56rpx; + height: 56rpx; +} + +.back-text { + font-size: 34rpx; + font-weight: 700; + color: #914584; +} + +/* 中间角色信息 */ +.nav-center { + display: flex; + align-items: center; + gap: 16rpx; +} + +.nav-avatar-wrap { + width: 64rpx; + height: 64rpx; + border-radius: 50%; + overflow: hidden; + border: 2rpx solid #E5E7EB; +} + +.nav-avatar { + width: 100%; + height: 100%; +} + +.nav-avatar-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #E9D5FF 0%, #FDF2F8 100%); + font-size: 28rpx; + font-weight: 600; + color: #8B5CF6; +} + +.nav-name { + font-size: 34rpx; + font-weight: 700; + color: #101828; +} + +.online-dot { + width: 16rpx; + height: 16rpx; + background: #00C950; + border-radius: 50%; +} + +/* 更多按钮 - 已移除,改为占位 */ +.nav-right-placeholder { + min-width: 160rpx; +} + +/* 聊天内容区域 - 使用100%高度填满父容器 */ +.chat-scroll { + height: 100%; + padding: 0 32rpx; + padding-top: 20rpx; + padding-bottom: 20rpx; + box-sizing: border-box; +} + +/* 隐藏滚动条 */ +.chat-scroll::-webkit-scrollbar { + display: none; + width: 0; + height: 0; +} + +/* 加载更多提示 */ +.load-more-hint { + padding: 20rpx 0; + text-align: center; +} + +.load-more-content { + display: flex; + align-items: center; + justify-content: center; + gap: 16rpx; +} + +.loading-spinner { + width: 32rpx; + height: 32rpx; + border: 4rpx solid #E5E7EB; + border-top-color: #b06ab3; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.load-more-text { + font-size: 24rpx; + color: #9CA3AF; +} + +.no-more-hint { + padding: 20rpx 0; + text-align: center; +} + +.no-more-hint text { + font-size: 24rpx; + color: #9CA3AF; +} + +/* 加密对话提示 */ +.encrypt-hint { + text-align: center; + padding: 32rpx 0 48rpx; +} + +.encrypt-hint text { + font-size: 24rpx; + color: #99A1AF; +} + +/* 聊天消息列表 */ +.chat-list { + display: flex; + flex-direction: column; + gap: 48rpx; +} + +/* 单条消息 */ +.chat-item { + display: flex; + gap: 24rpx; + align-items: flex-start; +} + +/* 用户消息靠右显示 */ +.chat-item.me { + justify-content: flex-end; +} + +/* 头像 */ +.avatar-wrap { + width: 88rpx; + height: 88rpx; + border-radius: 50%; + overflow: hidden; + flex-shrink: 0; + border: 2rpx solid rgba(255, 255, 255, 0.5); + box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1), 0 2rpx 6rpx rgba(0, 0, 0, 0.1); +} + +.chat-avatar { + width: 100%; + height: 100%; +} + +.avatar-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #E9D5FF 0%, #FDF2F8 100%); +} + +.avatar-placeholder.user { + background: #E5E7EB; +} + +.avatar-text { + font-size: 32rpx; + font-weight: 600; + color: #8B5CF6; +} + +.avatar-placeholder.user .avatar-text { + color: #6A7282; +} + +/* 用户头像特殊样式 - 确保显示 */ +.user-avatar { + display: flex !important; + visibility: visible !important; +} + +/* 消息内容 */ +.message-content { + max-width: 540rpx; + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.message-content.me { + align-items: flex-end; +} + +/* 聊天气泡 */ +.chat-bubble { + padding: 24rpx 40rpx; + word-break: break-all; + box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1), 0 2rpx 6rpx rgba(0, 0, 0, 0.1); +} + +/* AI消息气泡 - 白色背景,左上角小圆角 */ +.chat-bubble.other { + background: #FFFFFF; + border-radius: 12rpx 44rpx 44rpx 44rpx; +} + +/* 用户消息气泡 - 紫色背景,右上角小圆角 */ +.chat-bubble.me { + background: #914584; + border-radius: 44rpx 12rpx 44rpx 44rpx; +} + +/* 消息文字 */ +.chat-text { + font-size: 34rpx; + line-height: 1.625; +} + +.chat-bubble.other .chat-text { + color: #1E2939; +} + +.chat-bubble.me .chat-text { + color: #FFFFFF; +} + +/* 消息时间 */ +.message-time { + font-size: 22rpx; + color: #99A1AF; + padding: 0 8rpx; +} + +/* 消息操作区域 */ +.message-actions { + display: flex; + align-items: center; + gap: 16rpx; +} + +/* AI语音播放按钮 */ +.play-voice-btn { + width: 48rpx; + height: 48rpx; + background: rgba(145, 69, 132, 0.1); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.play-voice-icon { + width: 28rpx; + height: 28rpx; + opacity: 0.8; +} + +/* 图片消息气泡 */ +.chat-bubble-image { + max-width: 400rpx; + border-radius: 24rpx; + overflow: hidden; + box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1); +} + +.chat-bubble-image.other { + border-radius: 12rpx 24rpx 24rpx 24rpx; +} + +.chat-bubble-image.me { + border-radius: 24rpx 12rpx 24rpx 24rpx; +} + +.message-image { + width: 100%; + min-width: 200rpx; + max-width: 400rpx; + display: block; +} + +/* 语音消息气泡 */ +.chat-bubble.voice { + display: flex; + align-items: center; + gap: 16rpx; + min-width: 160rpx; + padding: 24rpx 32rpx; +} + +.chat-bubble.voice.other { + flex-direction: row; +} + +.chat-bubble.voice.me { + flex-direction: row-reverse; +} + +.voice-waves { + display: flex; + align-items: center; + gap: 6rpx; + height: 40rpx; +} + +.voice-wave-bar { + width: 6rpx; + height: 20rpx; + border-radius: 3rpx; + background: #9CA3AF; +} + +.chat-bubble.voice.me .voice-wave-bar { + background: rgba(255, 255, 255, 0.7); +} + +.voice-wave-bar:nth-child(1) { height: 16rpx; } +.voice-wave-bar:nth-child(2) { height: 28rpx; } +.voice-wave-bar:nth-child(3) { height: 20rpx; } + +/* 语音播放动画 */ +.chat-bubble.voice.playing .voice-wave-bar { + animation: voiceWave 0.8s ease-in-out infinite; +} + +.chat-bubble.voice.playing .voice-wave-bar:nth-child(1) { animation-delay: 0s; } +.chat-bubble.voice.playing .voice-wave-bar:nth-child(2) { animation-delay: 0.2s; } +.chat-bubble.voice.playing .voice-wave-bar:nth-child(3) { animation-delay: 0.4s; } + +@keyframes voiceWave { + 0%, 100% { transform: scaleY(1); } + 50% { transform: scaleY(1.8); } +} + +.voice-duration { + font-size: 28rpx; + color: #6B7280; +} + +.chat-bubble.voice.me .voice-duration { + color: rgba(255, 255, 255, 0.9); +} + +/* 语音识别文字 */ +.voice-recognized-text { + margin-top: 8rpx; + padding: 12rpx 16rpx; + background: rgba(0, 0, 0, 0.03); + border-radius: 12rpx; + max-width: 100%; +} + +.voice-recognized-text .recognizing-hint { + font-size: 24rpx; + color: #9CA3AF; +} + +.voice-recognized-text .recognized-text { + font-size: 26rpx; + color: #6B7280; + line-height: 1.5; + word-break: break-all; +} + +/* 礼物消息 */ +.gift-message { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.gift-message.me { + align-items: flex-end; +} + +/* 确保用户消息内容右对齐 */ +.message-content.me .gift-message { + align-items: flex-end; +} + +.gift-message-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 8rpx; + padding: 24rpx 32rpx; + background: linear-gradient(135deg, #FDF2F8 0%, #FCE7F3 100%); + border-radius: 24rpx; + box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1); +} + +.gift-message-image { + width: 80rpx; + height: 80rpx; +} + +.gift-message-text { + font-size: 26rpx; + color: #914584; +} + +/* 正在输入动画 */ +.chat-bubble.typing { + display: flex; + gap: 12rpx; + padding: 28rpx 40rpx; +} + +.typing-dot { + width: 16rpx; + height: 16rpx; + background: #9CA3AF; + border-radius: 50%; + animation: typing 1.4s infinite ease-in-out; +} + +.typing-dot:nth-child(1) { animation-delay: 0s; } +.typing-dot:nth-child(2) { animation-delay: 0.2s; } +.typing-dot:nth-child(3) { animation-delay: 0.4s; } + +@keyframes typing { + 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; } + 30% { transform: translateY(-12rpx); opacity: 1; } +} + +/* 底部占位 - 为最后一条消息留出空间 */ +.chat-bottom-space { + height: 40rpx; +} + +/* 底部输入区域 */ +.bottom-input-area { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: #FFFFFF; + border-top: 2rpx solid #F3F4F6; + box-shadow: 0 -10rpx 40rpx rgba(0, 0, 0, 0.03); + z-index: 100; + padding-bottom: env(safe-area-inset-bottom); +} + +.input-container { + display: flex; + align-items: center; + gap: 16rpx; + padding: 24rpx 32rpx; + padding-bottom: 16rpx; +} + +/* ==================== Figma设计样式 - 底部输入区域 ==================== */ + +/* Figma输入容器 */ +.figma-input-container { + display: flex; + align-items: center; + gap: 16rpx; + padding: 24rpx 20rpx; + padding-bottom: 20rpx; +} + +/* Figma语音按钮 - 40x40px 灰色圆形 */ +.figma-voice-btn { + width: 80rpx; + height: 80rpx; + background: #F3F4F6; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +/* Figma按钮图标 */ +.figma-btn-icon { + width: 80rpx; + height: 80rpx; +} + +/* Figma输入框 - 234x48px 浅灰背景 圆角16px */ +.figma-input-wrap { + flex: 1; + background: #F9FAFB; + border: 2rpx solid #F3F4F6; + border-radius: 32rpx; + padding: 0 32rpx; + height: 96rpx; + display: flex; + align-items: center; +} + +.figma-text-input { + width: 100%; + height: 100%; + font-size: 36rpx; + color: #101828; + font-family: Arial, sans-serif; +} + +.figma-input-placeholder { + color: rgba(10, 10, 10, 0.5); + font-size: 36rpx; + font-family: Arial, sans-serif; +} + +.figma-input-placeholder.warning { + color: #FF6B6B; +} + +/* Figma表情按钮 - 40x40px 灰色圆形 */ +.figma-emoji-btn { + width: 80rpx; + height: 80rpx; + background: transparent; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.figma-emoji-btn.active { + background: #E9D5FF; +} + +/* Figma发送按钮 */ +.figma-send-btn { + width: 80rpx; + height: 80rpx; + background: #914584; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.figma-send-btn .figma-btn-icon { + width: 44rpx; + height: 44rpx; +} + +/* Figma+号按钮 - 40x40px 粉色圆形背景 #FDF2F8 */ +.figma-add-btn { + width: 80rpx; + height: 80rpx; + background: transparent; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.figma-add-btn.active { + background: #FCE7F3; +} + +/* 语音按钮 */ +.voice-btn { + width: 80rpx; + height: 80rpx; + background: #F3F4F6; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.voice-icon { + width: 48rpx; + height: 48rpx; +} + +/* 输入框 */ +.input-wrap { + flex: 1; + background: #F9FAFB; + border: 2rpx solid #F3F4F6; + border-radius: 32rpx; + padding: 0 32rpx; + height: 96rpx; + display: flex; + align-items: center; +} + +.text-input { + width: 100%; + height: 100%; + font-size: 36rpx; + color: #101828; +} + +.input-placeholder { + color: rgba(10, 10, 10, 0.5); + font-size: 36rpx; +} + +/* 表情按钮 */ +.emoji-btn { + width: 80rpx; + height: 80rpx; + background: #F3F4F6; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.emoji-icon { + width: 48rpx; + height: 48rpx; +} + +/* 更多按钮 */ +.add-btn { + width: 80rpx; + height: 80rpx; + background: #F3F4F6; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.add-icon { + width: 48rpx; + height: 48rpx; +} + +/* 表情面板和更多面板显示时,移除底部安全区域 */ +.bottom-input-area.panel-open { + padding-bottom: 0; +} + +/* 语音录制按钮 */ +.voice-record-btn { + flex: 1; + background: #F3F4F6; + border: 2rpx solid #E5E7EB; + border-radius: 32rpx; + height: 96rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + font-weight: 500; + color: #374151; + transition: all 0.15s; +} + +.voice-record-btn:active, +.voice-record-btn.recording { + background: #E5E7EB; + transform: scale(0.98); +} + +/* 录音提示浮层 */ +.voice-recording-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.3); + display: flex; + align-items: center; + justify-content: center; + z-index: 200; +} + +.voice-recording-popup { + width: 320rpx; + height: 320rpx; + background: rgba(0, 0, 0, 0.8); + border-radius: 32rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 24rpx; +} + +.voice-recording-popup.cancel { + background: rgba(220, 38, 38, 0.9); +} + +.cancel-icon { + width: 80rpx; + height: 80rpx; + filter: brightness(0) invert(1); +} + +.voice-wave { + display: flex; + align-items: center; + justify-content: center; + gap: 12rpx; + height: 100rpx; +} + +.wave-bar { + width: 12rpx; + height: 40rpx; + background: #22C55E; + border-radius: 6rpx; + animation: wave 0.8s ease-in-out infinite; +} + +.wave-bar:nth-child(1) { animation-delay: 0s; height: 40rpx; } +.wave-bar:nth-child(2) { animation-delay: 0.1s; height: 60rpx; } +.wave-bar:nth-child(3) { animation-delay: 0.2s; height: 80rpx; } +.wave-bar:nth-child(4) { animation-delay: 0.3s; height: 60rpx; } +.wave-bar:nth-child(5) { animation-delay: 0.4s; height: 40rpx; } + +@keyframes wave { + 0%, 100% { transform: scaleY(1); } + 50% { transform: scaleY(1.5); } +} + +.voice-tip { + font-size: 28rpx; + color: #FFFFFF; +} + +.voice-duration-tip { + font-size: 48rpx; + font-weight: 700; + color: #FFFFFF; +} + +/* 发送按钮 */ +.send-btn { + width: 80rpx; + height: 80rpx; + background: #914584; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.send-icon { + width: 44rpx; + height: 44rpx; +} + +/* 按钮激活状态 */ +.voice-btn.active, +.emoji-btn.active, +.add-btn.active { + background: #E9D5FF; +} + +/* 表情面板 */ +.emoji-panel { + background: #FFFFFF; + border-top: 2rpx solid #F3F4F6; +} + +.emoji-scroll { + height: 480rpx; + padding: 24rpx; + box-sizing: border-box; +} + +.emoji-grid { + display: flex; + flex-wrap: wrap; +} + +.emoji-item { + width: 12.5%; + height: 88rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.emoji-text { + font-size: 56rpx; + line-height: 1; +} + +.emoji-item:active { + background: #F3F4F6; + border-radius: 16rpx; +} + +/* 表情面板底部操作 */ +.emoji-actions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 24rpx; + padding: 16rpx 32rpx; + border-top: 2rpx solid #F3F4F6; + padding-bottom: calc(16rpx + env(safe-area-inset-bottom)); + background: #FFFFFF; +} + +.emoji-delete { + width: 80rpx; + height: 72rpx; + background: #F3F4F6; + border-radius: 12rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.delete-icon { + width: 40rpx; + height: 40rpx; + transform: rotate(180deg); +} + +.emoji-send { + width: 120rpx; + height: 72rpx; + background: #E5E7EB; + border-radius: 12rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + color: #9CA3AF; +} + +.emoji-send.active { + background: #914584; + color: #FFFFFF; +} + +/* 更多功能面板 */ +.more-panel { + background: #F5F5F5; + border-top: 2rpx solid #E5E7EB; +} + +.more-panel-content { + padding: 40rpx 32rpx 24rpx; +} + +.more-grid { + display: flex; + justify-content: space-between; +} + +/* AI角色聊天:3个图标居中显示(Figma设计样式) */ +.more-grid.ai-chat-grid { + justify-content: center; + gap: 90rpx; + padding: 0 48rpx; +} + +.more-grid.second-row { + margin-top: 40rpx; +} + +.more-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 16rpx; + width: 25%; +} + +.more-icon-wrap { + width: 112rpx; + height: 112rpx; + background: #FFFFFF; + border-radius: 24rpx; + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +/* Figma设计样式 - 图标已包含白色圆角背景和阴影 */ +.more-icon-wrap.figma-style { + width: 128rpx; + height: 128rpx; + background: transparent; + border-radius: 0; + box-shadow: none; +} + +/* Figma图标样式 - 完整显示PNG图标(已包含背景和阴影) */ +.figma-action-icon { + width: 128rpx; + height: 128rpx; +} + +.new-tag { + position: absolute; + top: -8rpx; + right: -8rpx; + background: #FF6B35; + color: #FFFFFF; + font-size: 18rpx; + font-weight: 700; + padding: 4rpx 10rpx; + border-radius: 8rpx; + line-height: 1; +} + +.more-icon-img { + width: 56rpx; + height: 56rpx; +} + +.more-text { + font-size: 28rpx; + font-weight: 700; + color: #4A5565; +} + +/* 相册图标 - CSS绘制 */ +.album-icon { + width: 48rpx; + height: 40rpx; + position: relative; +} + +.album-frame { + width: 48rpx; + height: 40rpx; + border: 4rpx solid #914584; + border-radius: 6rpx; + position: absolute; + top: 0; + left: 0; + box-sizing: border-box; +} + +.album-sun { + width: 10rpx; + height: 10rpx; + background: #914584; + border-radius: 50%; + position: absolute; + top: 8rpx; + left: 8rpx; +} + +.album-mountain { + width: 0; + height: 0; + border-left: 12rpx solid transparent; + border-right: 12rpx solid transparent; + border-bottom: 14rpx solid #914584; + position: absolute; + bottom: 6rpx; + left: 12rpx; +} + +/* 常用语图标 - CSS绘制 */ +.quick-reply-icon { + width: 48rpx; + height: 40rpx; + position: relative; +} + +.reply-bubble1 { + width: 32rpx; + height: 22rpx; + border: 4rpx solid #914584; + border-radius: 6rpx; + position: absolute; + top: 0; + left: 0; + box-sizing: border-box; +} + +.reply-bubble2 { + width: 32rpx; + height: 22rpx; + border: 4rpx solid #914584; + border-radius: 6rpx; + position: absolute; + bottom: 0; + right: 0; + box-sizing: border-box; + background: #FFFFFF; +} + +/* 红包图标 - CSS绘制 */ +.red-packet-icon { + width: 40rpx; + height: 52rpx; + position: relative; +} + +.packet-body { + width: 40rpx; + height: 52rpx; + background: #E53935; + border-radius: 6rpx; + position: absolute; + top: 0; + left: 0; +} + +.packet-top { + width: 40rpx; + height: 20rpx; + background: #C62828; + border-radius: 6rpx 6rpx 0 0; + position: absolute; + top: 0; + left: 0; +} + +.packet-circle { + width: 16rpx; + height: 16rpx; + background: #FFD54F; + border-radius: 50%; + position: absolute; + top: 12rpx; + left: 12rpx; +} + +.more-panel-safe { + height: env(safe-area-inset-bottom); + background: #F5F5F5; +} + +/* 录音提示浮层 */ +.voice-recording-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.3); + display: flex; + align-items: center; + justify-content: center; + z-index: 200; +} + +.voice-recording-popup { + width: 320rpx; + height: 320rpx; + background: rgba(0, 0, 0, 0.8); + border-radius: 32rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 32rpx; +} + +.voice-recording-popup.cancel { + background: rgba(220, 38, 38, 0.9); +} + +.voice-wave { + display: flex; + align-items: center; + justify-content: center; + gap: 12rpx; + height: 100rpx; +} + +.wave-bar { + width: 12rpx; + height: 40rpx; + background: #FFFFFF; + border-radius: 6rpx; + animation: wave 0.8s ease-in-out infinite; +} + +.wave-bar:nth-child(1) { animation-delay: 0s; height: 40rpx; } +.wave-bar:nth-child(2) { animation-delay: 0.1s; height: 60rpx; } +.wave-bar:nth-child(3) { animation-delay: 0.2s; height: 80rpx; } +.wave-bar:nth-child(4) { animation-delay: 0.3s; height: 60rpx; } +.wave-bar:nth-child(5) { animation-delay: 0.4s; height: 40rpx; } + +@keyframes wave { + 0%, 100% { transform: scaleY(1); } + 50% { transform: scaleY(1.5); } +} + +.voice-tip { + font-size: 28rpx; + color: #FFFFFF; +} + +/* 礼物选择弹窗 */ +.gift-popup-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 300; + display: flex; + align-items: flex-end; +} + +.gift-popup { + width: 100%; + background: #FFFFFF; + border-radius: 32rpx 32rpx 0 0; + max-height: 80vh; + display: flex; + flex-direction: column; +} + +.gift-popup-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 32rpx; + border-bottom: 2rpx solid #F3F4F6; +} + +.gift-popup-title { + font-size: 36rpx; + font-weight: 700; + color: #101828; +} + +.gift-popup-close { + width: 56rpx; + height: 56rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.close-icon { + width: 40rpx; + height: 40rpx; +} + +.gift-list-scroll { + flex: 1; + max-height: 500rpx; + padding: 24rpx; +} + +.gift-grid { + display: flex; + flex-wrap: wrap; + gap: 24rpx; +} + +.gift-item { + width: calc(25% - 18rpx); + display: flex; + flex-direction: column; + align-items: center; + gap: 8rpx; + padding: 16rpx 8rpx; + border-radius: 16rpx; + border: 2rpx solid transparent; + transition: all 0.2s; +} + +.gift-item.selected { + background: #FDF2F8; + border-color: #914584; +} + +.gift-image { + width: 80rpx; + height: 80rpx; +} + +.gift-name { + font-size: 24rpx; + color: #374151; + text-align: center; +} + +.gift-price { + display: flex; + align-items: center; + gap: 4rpx; + font-size: 22rpx; + color: #914584; +} + +.price-icon { + width: 24rpx; + height: 24rpx; +} + +.gift-popup-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 24rpx 32rpx; + padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); + border-top: 2rpx solid #F3F4F6; +} + +.gift-balance { + display: flex; + align-items: center; + gap: 8rpx; +} + +.balance-label { + font-size: 28rpx; + color: #6B7280; +} + +.balance-icon { + width: 32rpx; + height: 32rpx; +} + +.balance-value { + font-size: 32rpx; + font-weight: 700; + color: #914584; +} + +.gift-send-btn { + width: 200rpx; + height: 80rpx; + background: #E5E7EB; + border-radius: 40rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + font-weight: 700; + color: #9CA3AF; +} + +.gift-send-btn.active { + background: #914584; + color: #FFFFFF; +} + +/* 免费畅聊提醒条 */ +.free-chat-bar { + position: fixed; + left: 0; + right: 0; + z-index: 99; + padding: 16rpx 32rpx; + background: #FFF5F5; + border-bottom: 2rpx solid #FFE4E4; + box-shadow: 0 2rpx 10rpx rgba(255, 77, 79, 0.05); +} + +.free-chat-content { + display: flex; + align-items: center; + justify-content: space-between; +} + +.free-chat-left { + display: flex; + align-items: center; + gap: 12rpx; +} + +.clock-icon { + width: 32rpx; + height: 32rpx; +} + +.free-chat-label { + font-size: 26rpx; + color: #FF4D4F; + font-weight: 500; +} + +.free-chat-time { + font-size: 28rpx; + color: #FF4D4F; + font-weight: bold; +} + +/* 免费畅聊选项样式 */ +.free-chat-option { + background: #F0FFF4; + border: 2rpx solid #C6F6D5; + box-shadow: 0 2rpx 4rpx -2rpx rgba(0, 0, 0, 0.1), 0 2rpx 6rpx rgba(0, 0, 0, 0.1); +} + +.free-icon { + background: linear-gradient(135deg, #F0FFF4 0%, #C6F6D5 100%); +} + +.free-btn { + background: #38A169; +} + +.free-btn text { + color: #fff; +} + +/* ==================== 解锁弹窗样式(与首页一致) ==================== */ + +/* 输入框警告样式 */ +.input-placeholder.warning { + color: #FF6B6B; +} + +/* 弹窗遮罩 */ +.unlock-popup-mask { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1001; + display: flex; + align-items: center; + justify-content: center; +} + +/* 弹窗主体 */ +.unlock-popup { + width: 680rpx; + background: #F8F9FC; + border-radius: 48rpx; + overflow: hidden; + position: relative; + box-shadow: 0 50rpx 100rpx -24rpx rgba(0, 0, 0, 0.25); + animation: unlockPopupIn 0.3s ease-out; +} + +@keyframes unlockPopupIn { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +/* 关闭按钮 */ +.unlock-popup-close { + position: absolute; + top: 32rpx; + right: 32rpx; + width: 32rpx; + height: 32rpx; + display: flex; + justify-content: center; + align-items: center; + opacity: 0.7; + z-index: 10; +} + +.unlock-popup-close text { + font-size: 40rpx; + color: #0A0A0A; + line-height: 1; + font-weight: 300; +} + +/* 顶部白色区域 */ +.unlock-popup-header { + background: #fff; + padding: 48rpx 48rpx 40rpx; + border-radius: 0 0 60rpx 60rpx; + box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + align-items: center; +} + +/* 头像容器 */ +.unlock-avatar-container { + position: relative; + width: 192rpx; + height: 192rpx; + margin-bottom: 32rpx; +} + +.unlock-avatar-wrap { + width: 192rpx; + height: 192rpx; + border-radius: 50%; + overflow: hidden; + border: 4rpx solid #fff; + box-shadow: 0 20rpx 30rpx -6rpx rgba(0, 0, 0, 0.1), 0 8rpx 12rpx -4rpx rgba(0, 0, 0, 0.1); +} + +.unlock-avatar { + width: 100%; + height: 100%; +} + +/* 锁图标 */ +.unlock-lock-icon { + position: absolute; + bottom: -8rpx; + right: -8rpx; + width: 56rpx; + height: 56rpx; + background: #fff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4rpx 12rpx -2rpx rgba(0, 0, 0, 0.1), 0 8rpx 12rpx -2rpx rgba(0, 0, 0, 0.1); +} + +.unlock-lock-icon image { + width: 32rpx; + height: 32rpx; +} + +/* 标题 */ +.unlock-title { + text-align: center; + font-size: 42rpx; + font-weight: 900; + color: #101828; + line-height: 1.25; + letter-spacing: -0.5rpx; +} + +.unlock-title .highlight { + color: #914584; +} + +/* 选项区域 */ +.unlock-options { + padding: 40rpx 40rpx 48rpx; + display: flex; + flex-direction: column; + gap: 24rpx; +} + +/* 选项卡通用样式 */ +.unlock-option-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 32rpx; + height: 140rpx; + border-radius: 28rpx; +} + +.option-left { + display: flex; + align-items: center; + gap: 28rpx; +} + +/* 图标容器 */ +.option-icon { + width: 88rpx; + height: 88rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +/* 爱心图标 */ +.hearts-icon { + background: linear-gradient(135deg, #FEF2F2 0%, #FFE2E2 100%); + box-shadow: inset 0 4rpx 8rpx rgba(0, 0, 0, 0.05); +} + +.hearts-icon image { + width: 44rpx; + height: 44rpx; +} + +/* 金钱图标 */ +.money-icon { + background: rgba(255, 255, 255, 0.2); +} + +.money-icon .money-symbol { + font-size: 48rpx; + font-weight: 900; + color: #fff; +} + +/* 选项信息 */ +.option-info { + display: flex; + flex-direction: column; + gap: 0; +} + +.option-title { + font-size: 40rpx; + font-weight: 900; + color: #101828; + line-height: 1.5; + letter-spacing: -0.5rpx; +} + +.option-desc { + font-size: 30rpx; + font-weight: 700; + color: #6A7282; + line-height: 1.5; +} + +.option-info.light .option-title { + color: #fff; +} + +.option-info.light .option-desc { + color: rgba(255, 255, 255, 0.95); +} + +/* 按钮 */ +.option-btn { + padding: 16rpx 40rpx; + border-radius: 100rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.option-btn text { + font-size: 36rpx; + font-weight: 900; + letter-spacing: -0.5rpx; +} + +/* 爱心选项 */ +.hearts-option { + background: #fff; + border: 2rpx solid #F3F4F6; + box-shadow: 0 2rpx 4rpx -2rpx rgba(0, 0, 0, 0.1), 0 2rpx 6rpx rgba(0, 0, 0, 0.1); +} + +.hearts-btn { + background: #F3F4F6; +} + +.hearts-btn text { + color: #4A5565; +} + +/* 现金选项 */ +.money-option { + background: linear-gradient(180deg, #914584 0%, #7A3A6F 100%); + box-shadow: 0 8rpx 12rpx -8rpx rgba(243, 232, 255, 1), 0 20rpx 30rpx -6rpx rgba(243, 232, 255, 1); +} + +.money-btn { + background: #fff; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1); +} + +.money-btn text { + color: #914584; +} + +/* 暂不需要 */ +.unlock-cancel { + text-align: center; + padding: 20rpx 0 0; +} + +.unlock-cancel text { + font-size: 32rpx; + font-weight: 700; + color: #99A1AF; + letter-spacing: 0.5rpx; +} diff --git a/pages/chat/chat.js b/pages/chat/chat.js new file mode 100644 index 0000000..092869c --- /dev/null +++ b/pages/chat/chat.js @@ -0,0 +1,714 @@ +// pages/chat/chat.js +// 消息列表页面 - 显示会话列表 + +const app = getApp() +const api = require('../../utils/api') +const util = require('../../utils/util') +const proactiveMessage = require('../../utils/proactiveMessage') + +Page({ + data: { + // 系统消息(静态) + systemMessages: [ + { + id: 'sys-1', + name: '推广收益', + type: 'system', + icon: '/images/icon-trending-up.png', + gradient: 'pink', + preview: '您的好友开通了会员,您获得收益!', + time: '刚刚', + unread: 0 + }, + { + id: 'sys-2', + name: '系统通知', + type: 'system', + icon: '/images/icon-bell.png', + gradient: 'purple', + preview: '欢迎来到心伴俱乐部', + time: '', + unread: 0 + } + ], + + // AI会话列表 + conversations: [], + + // 总未读消息数 + totalUnread: 0, + + // 加载状态 + loading: true, + error: null, + auditStatus: 0, + + // 免费畅聊相关 + freeTime: null, + countdownText: '' + }, + + onLoad() { + this.loadConversations() + }, + + onShow() { + wx.hideTabBar({ animation: false }) + const app = getApp() + this.setData({ + auditStatus: app.globalData.auditStatus + }) + + // 检查免费畅聊时间 + this.checkFreeTime() + + // 每次显示时刷新列表 + // 增加延迟,确保标记已读API有时间完成(从聊天详情页返回时) + if (!this.data.loading) { + setTimeout(() => { + this.loadConversations() + }, 500) // 增加到500ms,确保标记已读API完成 + } + }, + + onPullDownRefresh() { + this.checkFreeTime() + this.loadConversations().then(() => { + wx.stopPullDownRefresh() + }) + }, + + /** + * 检查免费畅聊时间 + */ + async checkFreeTime() { + if (!app.globalData.isLoggedIn) return + + try { + const res = await api.chat.getFreeTime() + console.log('[chat] 免费畅聊时间响应:', res) + if (res.success && res.data) { + this.setData({ + freeTime: res.data + }) + + if (res.data.isActive && res.data.remainingSeconds > 0) { + this.startCountdown(res.data.remainingSeconds) + } else { + this.stopCountdown() + } + } + } catch (err) { + console.error('[chat] 获取免费畅聊时间失败:', err) + } + }, + + /** + * 开始倒计时 + */ + startCountdown(seconds) { + this.stopCountdown() + + let remaining = seconds + this.setData({ + countdownText: this.formatSeconds(remaining) + }) + + this.countdownTimer = setInterval(() => { + remaining-- + if (remaining <= 0) { + this.stopCountdown() + this.setData({ + 'freeTime.isActive': false, + 'freeTime.remainingSeconds': 0 + }) + } else { + this.setData({ + countdownText: this.formatSeconds(remaining) + }) + } + }, 1000) + }, + + /** + * 停止倒计时 + */ + stopCountdown() { + if (this.countdownTimer) { + clearInterval(this.countdownTimer) + this.countdownTimer = null + } + this.setData({ countdownText: '' }) + }, + + /** + * 格式化秒数为分钟(向上取整) + */ + formatSeconds(s) { + const m = Math.ceil(s / 60) + return m + }, + + onHide() { + this.stopCountdown() + }, + + onUnload() { + this.stopCountdown() + }, + + /** + * 加载会话列表 + */ + async loadConversations() { + // 检查登录状态 + if (!app.globalData.isLoggedIn) { + this.setData({ + conversations: [], + loading: false + }) + return + } + + this.setData({ loading: true, error: null }) + + try { + // 并行获取会话列表和主动推送消息 + const [convRes, proactiveMessages] = await Promise.all([ + api.chat.getConversations(), + this.getProactiveMessagesForList() + ]) + + console.log('[chat] 会话列表API响应:', JSON.stringify(convRes)) + console.log('[chat] 主动推送消息:', JSON.stringify(proactiveMessages)) + + if (convRes.success && convRes.data) { + // 转换数据格式 + let conversations = convRes.data.map(conv => this.transformConversation(conv)) + console.log('[chat] 转换后的会话数量:', conversations.length) + + // 将主动推送消息合并到会话列表 + if (proactiveMessages && proactiveMessages.length > 0) { + console.log('[chat] 开始合并主动推送消息,数量:', proactiveMessages.length) + conversations = this.mergeProactiveMessages(conversations, proactiveMessages) + console.log('[chat] 合并后的会话数量:', conversations.length) + } + + // 按时间排序(最新的在前) + conversations.sort((a, b) => { + return new Date(b.updatedAt) - new Date(a.updatedAt) + }) + + // 计算总未读消息数 + const totalUnread = conversations.reduce((sum, conv) => sum + (conv.unread || 0), 0) + + this.setData({ + conversations, + totalUnread, + loading: false + }) + + console.log('[chat] 最终会话列表:', conversations.map(c => ({ id: c.id, name: c.name, unread: c.unread, isProactive: c.isProactive }))) + } else { + throw new Error(convRes.message || '加载失败') + } + } catch (err) { + console.error('加载会话列表失败', err) + + // 如果是401错误,不显示错误提示,因为会话列表会被清空 + if (err.code === 401) { + this.setData({ + conversations: [], + loading: false + }) + return + } + + this.setData({ + loading: false, + error: err.message || '加载失败' + }) + } + }, + + /** + * 转换会话数据格式 + * @param {object} conv - 后端会话数据 + */ + transformConversation(conv) { + const character = conv.character || {} + const config = require('../../config/index') + + // 处理头像URL - 如果是相对路径,拼接服务器地址 + let avatarUrl = character.avatar || '' + if (avatarUrl && avatarUrl.startsWith('/characters/')) { + const baseUrl = config.API_BASE_URL.replace('/api', '') + avatarUrl = baseUrl + avatarUrl + } + + // 处理预览消息 - 优先显示最后消息,否则显示默认提示 + let preview = conv.last_message + if (!preview || preview.trim() === '') { + preview = '点击开始聊天~' + } + + return { + id: conv.id, + characterId: conv.character_id || conv.target_id, + name: character.name || 'AI助手', + type: 'ai', + avatar: avatarUrl || 'https://ai-c.maimanji.com/images/default-avatar.png', + preview: preview, + time: util.formatRelativeTime(conv.updated_at), + updatedAt: conv.updated_at, + unread: conv.unread_count || 0, + isOnline: true + } + }, + + /** + * 获取主动推送消息列表(用于合并到会话列表) + * 直接调用API获取,不依赖缓存 + */ + async getProactiveMessagesForList() { + try { + // 直接调用API获取待推送消息 + const res = await api.proactiveMessage.getPending() + console.log('[chat] 主动推送消息API响应:', JSON.stringify(res)) + + if (res.success && res.data && Array.isArray(res.data)) { + console.log('[chat] 获取到主动推送消息:', res.data.length, '条') + return res.data + } + + console.log('[chat] 主动推送消息API返回空或格式错误') + return [] + } catch (err) { + console.log('[chat] 获取主动推送消息失败', err) + return [] + } + }, + + /** + * 将主动推送消息合并到会话列表 + * @param {Array} conversations - 现有会话列表 + * @param {Array} proactiveMessages - 主动推送消息列表 + */ + mergeProactiveMessages(conversations, proactiveMessages) { + if (!proactiveMessages || proactiveMessages.length === 0) { + console.log('[chat] 没有主动推送消息需要合并') + return conversations + } + + const config = require('../../config/index') + const baseUrl = config.API_BASE_URL.replace('/api', '') + + console.log('[chat] 开始合并主动推送消息,消息数:', proactiveMessages.length) + + // 遍历主动推送消息 + proactiveMessages.forEach((msg, index) => { + console.log(`[chat] 处理第${index + 1}条消息:`, { + character_id: msg.character_id, + character_name: msg.character_name, + content: msg.content?.substring(0, 20) + '...' + }) + + // 查找是否已有该角色的会话 + const existingConvIndex = conversations.findIndex(c => { + // 兼容不同的ID格式(字符串和数字) + return String(c.characterId) === String(msg.character_id) + }) + + if (existingConvIndex >= 0) { + // 已有会话:只更新预览消息和未读数,不修改时间(避免列表位置跳动) + const existingConv = conversations[existingConvIndex] + console.log(`[chat] 找到已有会话:`, existingConv.name, '更新消息(保持原有时间排序)') + + existingConv.preview = msg.content + existingConv.unread = (existingConv.unread || 0) + 1 + // 注意:不修改 updatedAt 和 time,保持会话原有的排序位置 + existingConv.isProactive = true // 标记为主动推送消息 + } else { + // 没有会话:创建新的会话项 + console.log(`[chat] 创建新会话:`, msg.character_name) + + let avatarUrl = msg.character_logo || '' + if (avatarUrl && avatarUrl.startsWith('/characters/')) { + avatarUrl = baseUrl + avatarUrl + } + + const newConv = { + id: `proactive_${msg.character_id}`, + characterId: msg.character_id, + name: msg.character_name || 'AI助手', + type: 'ai', + avatar: avatarUrl || 'https://ai-c.maimanji.com/images/default-avatar.png', + preview: msg.content, + time: util.formatRelativeTime(msg.sent_at), + updatedAt: msg.sent_at, + unread: 1, + isOnline: true, + isProactive: true // 标记为主动推送消息 + } + + conversations.push(newConv) + console.log(`[chat] 新会话已添加:`, newConv.name, newConv.id) + } + }) + + console.log('[chat] 合并完成,最终会话数:', conversations.length) + return conversations + }, + + /** + * 点击免费畅聊条 + */ + onFreeChatTap() { + wx.showModal({ + title: '免费畅聊', + content: '您当前拥有免费畅聊特权,可以无消耗与 AI 角色对话。', + showCancel: false, + confirmText: '我知道了' + }) + }, + + /** + * 返回上一页 + */ + goBack() { + wx.navigateBack({ + fail: () => { + wx.switchTab({ url: '/pages/index/index' }) + } + }) + }, + + /** + * 点击消息项 + */ + async onMessageTap(e) { + const { id, type } = e.currentTarget.dataset + + if (type === 'system') { + // 系统消息 + this.handleSystemMessage(id) + } else { + // AI会话 + const conversation = this.data.conversations.find(c => c.id === id) + if (conversation) { + // 处理主动推送消息创建的虚拟会话(ID以proactive_开头) + const isProactiveConv = id.startsWith('proactive_') + const conversationId = isProactiveConv ? '' : id + + // 跳转到聊天详情页(标记已读在详情页onLoad时调用) + wx.navigateTo({ + url: `/pages/chat-detail/chat-detail?id=${conversation.characterId}&conversationId=${conversationId}&name=${encodeURIComponent(conversation.name)}` + }) + } + } + }, + + /** + * 更新本地未读数 + * @param {string} conversationId - 会话ID + * @param {number} unread - 新的未读数 + */ + updateLocalUnread(conversationId, unread) { + const conversations = this.data.conversations.map(conv => { + if (conv.id === conversationId) { + return { ...conv, unread } + } + return conv + }) + + // 重新计算总未读数 + const totalUnread = conversations.reduce((sum, conv) => sum + (conv.unread || 0), 0) + + this.setData({ conversations, totalUnread }) + }, + + /** + * 处理系统消息点击 + */ + handleSystemMessage(id) { + if (id === 'sys-1') { + // 推广收益 + wx.navigateTo({ url: '/pages/commission/commission' }) + } else if (id === 'sys-2') { + // 系统通知 + wx.showToast({ title: '暂无新通知', icon: 'none' }) + } + }, + + /** + * 标记会话已读 + */ + async markAsRead(conversationId) { + try { + await api.chat.markAsRead(conversationId) + + // 更新本地状态 + const conversations = this.data.conversations.map(conv => { + if (conv.id === conversationId) { + return { ...conv, unread: 0 } + } + return conv + }) + + // 重新计算总未读数 + const totalUnread = conversations.reduce((sum, conv) => sum + (conv.unread || 0), 0) + + this.setData({ conversations, totalUnread }) + } catch (err) { + console.log('标记已读失败', err) + } + }, + + /** + * 删除会话(长按) + */ + onMessageLongPress(e) { + const { id, type } = e.currentTarget.dataset + + if (type === 'system') return + + wx.showActionSheet({ + itemList: ['清空聊天记录', '删除会话(从列表移除)'], + success: (res) => { + if (res.tapIndex === 0) { + // 清空聊天记录(保留会话) + this.clearChatHistory(id) + } else if (res.tapIndex === 1) { + // 删除会话(从列表移除) + this.deleteConversation(id) + } + } + }) + }, + + /** + * 滑动开始 + */ + onTouchStart(e) { + this.touchStartX = e.touches[0].clientX + this.touchStartY = e.touches[0].clientY + this.touchStartTime = Date.now() + }, + + /** + * 滑动中 + */ + onTouchMove(e) { + const moveX = e.touches[0].clientX - this.touchStartX + const moveY = e.touches[0].clientY - this.touchStartY + + // 如果垂直滑动大于水平滑动,不处理 + if (Math.abs(moveY) > Math.abs(moveX)) return + }, + + /** + * 滑动结束 + */ + onTouchEnd(e) { + const endX = e.changedTouches[0].clientX + const moveX = endX - this.touchStartX + const index = e.currentTarget.dataset.index + const id = e.currentTarget.dataset.id + + // 先关闭其他已展开的项 + this.closeAllSwipe(index) + + // 左滑超过50px显示删除按钮 + if (moveX < -50) { + this.setSwipeState(index, true) + } else if (moveX > 50) { + // 右滑关闭 + this.setSwipeState(index, false) + } + }, + + /** + * 设置滑动状态 + */ + setSwipeState(index, swiped) { + const conversations = this.data.conversations + if (conversations[index]) { + conversations[index].swiped = swiped + this.setData({ conversations }) + } + }, + + /** + * 关闭所有滑动项(除了指定的) + */ + closeAllSwipe(exceptIndex) { + const conversations = this.data.conversations.map((conv, idx) => { + if (idx !== exceptIndex && conv.swiped) { + return { ...conv, swiped: false } + } + return conv + }) + this.setData({ conversations }) + }, + + /** + * 滑动删除按钮点击 + */ + onSwipeDelete(e) { + const { id, index } = e.currentTarget.dataset + this.deleteConversation(id) + }, + + /** + * 删除会话 + * 完全删除会话,包括聊天记录,会话从列表中移除 + */ + async deleteConversation(conversationId) { + const confirmed = await util.showConfirm({ + title: '删除会话', + content: '确定要删除这个会话吗?会话将从列表中移除,聊天记录也会被清空。' + }) + + if (!confirmed) return + + wx.showLoading({ title: '删除中...' }) + + try { + // 调用后端API删除会话 + const res = await api.chat.deleteConversation(conversationId) + + if (res.success || res.code === 0) { + // 本地删除 + const conversations = this.data.conversations.filter(c => c.id !== conversationId) + this.setData({ conversations }) + util.showSuccess('已删除') + } else { + throw new Error(res.message || '删除失败') + } + } catch (err) { + console.error('删除会话失败:', err) + // 即使API失败,也从本地删除 + const conversations = this.data.conversations.filter(c => c.id !== conversationId) + this.setData({ conversations }) + util.showSuccess('已删除') + } finally { + wx.hideLoading() + } + }, + + /** + * 清空聊天记录 + * 只清空聊天记录,不删除会话 + * 会话仍然显示在消息列表中,只是聊天记录被清空 + */ + async clearChatHistory(conversationId) { + const confirmed = await util.showConfirm({ + title: '清空记录', + content: '确定要清空聊天记录吗?此操作不可恢复。会话仍会保留在列表中。' + }) + + if (!confirmed) return + + // 找到对应的会话,获取角色ID + const conversation = this.data.conversations.find(c => c.id === conversationId) + if (!conversation || !conversation.characterId) { + util.showError('会话信息错误') + return + } + + wx.showLoading({ title: '清空中...' }) + + try { + // 调用后端API清空聊天记录(使用角色ID) + const res = await api.chat.clearChatHistory(conversation.characterId) + + if (res.success || res.code === 0) { + // 更新本地会话列表,清空预览消息 + const conversations = this.data.conversations.map(conv => { + if (conv.id === conversationId) { + return { + ...conv, + preview: '点击开始聊天~', + unread: 0 + } + } + return conv + }) + + this.setData({ conversations }) + util.showSuccess('已清空') + } else { + throw new Error(res.message || '清空失败') + } + } catch (err) { + console.error('清空聊天记录失败:', err) + util.showError('清空失败,请重试') + } finally { + wx.hideLoading() + } + }, + + /** + * 切换Tab - 需要登录的页面检查登录状态 + */ + switchTab(e) { + const path = e.currentTarget.dataset.path + if (path) { + const app = getApp() + + // 消息页面需要登录 + if (path === '/pages/chat/chat') { + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ + url: '/pages/login/login?redirect=' + encodeURIComponent(path) + }) + return + } + } + wx.switchTab({ url: path }) + } + }, + + /** + * 需要登录时的回调 + */ + onAuthRequired() { + wx.showModal({ + title: '提示', + content: '请先登录后查看消息', + confirmText: '去登录', + confirmColor: '#b06ab3', + success: (res) => { + if (res.confirm) { + app.wxLogin().then(() => { + this.loadConversations() + }).catch(err => { + util.showError('登录失败') + }) + } + } + }) + }, + + /** + * 检查AI角色主动推送消息 + */ + async checkProactiveMessages() { + if (!app.globalData.isLoggedIn) { + return + } + + try { + const messages = await proactiveMessage.checkAndShowMessages({ + onNewMessages: (msgs) => { + // 收到新消息时刷新会话列表 + this.loadConversations() + } + }) + + console.log('[chat] 主动推送消息检查完成,消息数:', messages.length) + } catch (err) { + console.log('[chat] 检查主动推送消息失败', err) + } + } +}) diff --git a/pages/chat/chat.json b/pages/chat/chat.json new file mode 100644 index 0000000..b16ef3e --- /dev/null +++ b/pages/chat/chat.json @@ -0,0 +1,4 @@ +{ + "navigationStyle": "custom", + "usingComponents": {} +} diff --git a/pages/chat/chat.wxml b/pages/chat/chat.wxml new file mode 100644 index 0000000..c735da2 --- /dev/null +++ b/pages/chat/chat.wxml @@ -0,0 +1,108 @@ + + + + + + 消息 + + + + + + + + + + + + + + + + + + + + {{item.unread}} + + + + {{item.name}} + {{item.time}} + + {{item.preview}} + + + + + + + + + + + + {{item.unread}} + + + + {{item.name}} + {{item.time}} + + {{item.preview}} + + + + + + + 删除 + + + + + + + 加载中... + + + + + 暂无聊天记录 + 去陪伴页面挑选聊天的伙伴吧~ + + + + + + + + 陪伴 + + + + 文娱 + + + + 服务 + + + + + + 消息 + + + + 我的 + + + diff --git a/pages/chat/chat.wxss b/pages/chat/chat.wxss new file mode 100644 index 0000000..00352c0 --- /dev/null +++ b/pages/chat/chat.wxss @@ -0,0 +1,292 @@ +page { + width: 100%; + overflow-x: hidden; + background: #fff; +} + +.page-container { + min-height: 100vh; + display: flex; + flex-direction: column; + width: 100%; + overflow-x: hidden; + background: #fff; +} + +/* 顶部导航栏已移除,改用全局 unified-header */ + +/* 免费畅聊提醒条 (嵌入式) */ +.free-chat-banner { + margin: 20rpx 40rpx 10rpx; + padding: 24rpx 32rpx; + background: #FFF5F5; + border-radius: 20rpx; + border: 2rpx solid #FFE4E4; +} + +.free-chat-banner-content { + display: flex; + align-items: center; + gap: 16rpx; +} + +.banner-clock-icon { + width: 32rpx; + height: 32rpx; +} + +.banner-text { + font-size: 28rpx; + color: #FF4D4F; + font-weight: 500; +} + +/* 消息列表 */ +.message-list { + flex: 1; + padding-top: 174rpx; /* 只留出 header 的位置 */ + padding-bottom: 160rpx; +} + +.message-item { + display: flex; + align-items: flex-start; + padding: 32rpx 40rpx; + border-bottom: 2rpx solid #f3f4f6; + gap: 32rpx; +} + +/* 头像区域 */ +.avatar-section { + position: relative; + flex-shrink: 0; +} + +.avatar-wrapper { + width: 136rpx; + height: 136rpx; + border-radius: 32rpx; + overflow: hidden; +} + +.avatar-wrapper.ai { + border: 2rpx solid #f3f4f6; + box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1), 0 2rpx 4rpx -2rpx rgba(0, 0, 0, 0.1); +} + +.avatar-wrapper.system { + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8rpx 12rpx -2rpx rgba(0, 0, 0, 0.1); +} + +.avatar-wrapper.system.pink { + background: linear-gradient(180deg, #ff9a9e 0%, #fecfef 100%); +} + +.avatar-wrapper.system.purple { + background: linear-gradient(180deg, #a18cd1 0%, #fbc2eb 100%); +} + +.avatar-image { + width: 100%; + height: 100%; +} + +.avatar-icon { + width: 56rpx; + height: 56rpx; +} + +.badge { + position: absolute; + top: -16rpx; + right: -8rpx; + min-width: 48rpx; + height: 48rpx; + background: #ff3b30; + border: 2rpx solid #fff; + border-radius: 24rpx; + display: flex; + align-items: center; + justify-content: center; + padding: 0 14rpx; + box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1), 0 2rpx 4rpx -2rpx rgba(0, 0, 0, 0.1); + font-size: 26rpx; + font-weight: bold; + color: #fff; +} + +/* 内容区域 */ +.message-content { + flex: 1; + min-width: 0; + overflow: hidden; + padding-top: 12rpx; +} + +.message-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 8rpx; +} + +.message-name { + font-size: 36rpx; + font-weight: bold; + color: #101828; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.message-time { + font-size: 26rpx; + color: #99a1af; + flex-shrink: 0; + margin-left: 16rpx; +} + +.message-preview { + font-size: 30rpx; + color: #6a7282; + line-height: 1.6; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 底部导航栏 - 完全匹配Figma设计 */ +.custom-tabbar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 194rpx; + background: #fff; + display: flex; + align-items: flex-start; + justify-content: space-around; + padding-top: 24rpx; + z-index: 999; + border-top: 2rpx solid #F3F4F6; +} + +.tabbar-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12rpx; + width: 150rpx; + height: 120rpx; +} + +.tabbar-icon { + width: 68rpx; + height: 68rpx; +} + +.tabbar-text { + font-family: Arial, sans-serif; + font-size: 40rpx; + font-weight: 700; + color: #A58AA5; + line-height: 1; +} + +.tabbar-text.active { + color: #B06AB3; +} + +.message-icon-wrapper { + width: 68rpx; + height: 68rpx; +} + +.message-icon-wrapper .tabbar-icon { + width: 68rpx; + height: 68rpx; +} + + +/* 加载状态 */ +.loading-tip { + display: flex; + justify-content: center; + padding: 40rpx; + color: #99a1af; + font-size: 28rpx; +} + +/* 空状态 */ +.empty-tip { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 120rpx 40rpx; + color: #99a1af; + font-size: 30rpx; +} + +.empty-sub { + font-size: 26rpx; + margin-top: 16rpx; + color: #c0c5ce; +} + +/* 滑动删除样式 */ +.swipe-container { + position: relative; + overflow: hidden; +} + +.swipe-content { + position: relative; + z-index: 2; + background: #fff; + transition: transform 0.2s ease-out; +} + +.swipe-content.swiped { + transform: translateX(-160rpx); +} + +.swipe-actions { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 160rpx; + display: flex; + align-items: stretch; + z-index: 1; + opacity: 0; + transition: opacity 0.2s ease-out; +} + +.swipe-actions.show { + opacity: 1; +} + +.action-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + +.action-btn.delete { + background: linear-gradient(135deg, #ff6b6b 0%, #ee5a5a 100%); +} + +.action-btn text { + color: #fff; + font-size: 28rpx; + font-weight: 500; +} diff --git a/pages/city-activities/city-activities.js b/pages/city-activities/city-activities.js new file mode 100644 index 0000000..f784ae9 --- /dev/null +++ b/pages/city-activities/city-activities.js @@ -0,0 +1,358 @@ +// pages/city-activities/city-activities.js - 同城活动页面 +const api = require('../../utils/api') +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + loading: false, + + // 选中的城市 + selectedCity: '深圳市', + + // 活动列表 + activityList: [], + + // 二维码弹窗 + showQrcodeModal: false, + qrcodeImageUrl: '' // 二维码图片URL,从后端获取 + }, + + onLoad(options) { + // 计算导航栏高度 + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight + }) + + // 从文娱首页获取城市信息(通过全局数据或页面参数) + const app = getApp() + // 解码URL参数中的城市名称 + let selectedCity = '深圳市' + if (options.city) { + selectedCity = decodeURIComponent(options.city) + } else if (app.globalData.selectedCity) { + selectedCity = app.globalData.selectedCity + } + + console.log('[city-activities] 接收到的城市:', selectedCity) + this.setData({ selectedCity }) + this.loadActivityList() + }, + + /** + * 页面显示时检查城市是否变化 + */ + onShow() { + const app = getApp() + if (app.globalData.selectedCity && app.globalData.selectedCity !== this.data.selectedCity) { + console.log('[city-activities] 城市已变更:', app.globalData.selectedCity) + this.setData({ selectedCity: app.globalData.selectedCity }) + this.loadActivityList() + } + }, + + /** + * 返回上一页 + */ + onBack() { + wx.navigateBack() + }, + + /** + * 加载活动列表 - 根据categoryName筛选同城活动 + */ + async loadActivityList() { + this.setData({ loading: true }) + + try { + const res = await api.activity.getList({ + city: this.data.selectedCity, + limit: 50 // 获取更多数据用于前端筛选 + }) + + if (res.success && res.data && res.data.list) { + // 前端筛选:只显示categoryName为"同城活动"的活动 + const allActivities = res.data.list + const cityActivities = allActivities.filter(item => item.categoryName === '同城活动') + + // 转换数据格式 + const activityList = cityActivities.map(item => ({ + id: item.id, + title: item.title, + date: this.formatDate(item.activityDate), + location: item.location || '', + venue: item.venue || '', + image: item.coverImage || '/images/activity-default.jpg', + heat: item.heat || 0, // 使用后端返回的热度字段 + isFree: item.priceType === 'free', + price: item.priceText || '', + status: item.status || ((item.current_participants || item.currentParticipants || 0) >= (item.max_participants || item.maxParticipants || 0) && (item.max_participants || item.maxParticipants || 0) > 0 ? 'full' : 'upcoming'), + activityGuideQrcode: item.activityGuideQrcode || item.activity_guide_qrcode || '' + })) + + console.log('[city-activities] 同城活动加载成功,数量:', activityList.length) + this.setData({ activityList }) + } else { + this.setData({ activityList: [] }) + } + } catch (err) { + console.error('加载活动列表失败', err) + wx.showToast({ + title: '加载失败', + icon: 'none' + }) + this.setData({ activityList: [] }) + } finally { + this.setData({ loading: false }) + } + }, + + /** + * 加载模拟数据(降级方案) + */ + loadMockActivities() { + // 使用空数据,等待后端API返回真实数据 + const mockActivities = [] + + this.setData({ activityList: mockActivities }) + }, + + /** + * 格式化日期 + */ + formatDate(dateStr) { + if (!dateStr) return '' + const date = new Date(dateStr) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}年${month}月${day}日` + }, + + /** + * 选择城市 + */ + onCitySelect() { + wx.navigateTo({ + url: `/pages/city-selector/city-selector?current=${encodeURIComponent(this.data.selectedCity)}` + }) + }, + + /** + * 加入同城群 + */ + onJoinCityGroup() { + // TODO: 从后端获取二维码图片URL + // 暂时使用占位图片 + this.setData({ + showQrcodeModal: true, + qrcodeImageUrl: '/images/city-group-qrcode-placeholder.png' // 占位图片 + }) + }, + + /** + * 关闭二维码弹窗 + */ + onCloseQrcodeModal() { + this.setData({ + showQrcodeModal: false + }) + }, + + /** + * 保存二维码 + */ + onSaveQrcode() { + const { qrcodeImageUrl } = this.data + + if (!qrcodeImageUrl) { + wx.showToast({ + title: '二维码加载中', + icon: 'none' + }) + return + } + + wx.showLoading({ title: '保存中...' }) + + // 下载图片到本地 + wx.downloadFile({ + url: qrcodeImageUrl, + success: (res) => { + if (res.statusCode === 200) { + // 保存到相册 + wx.saveImageToPhotosAlbum({ + filePath: res.tempFilePath, + success: () => { + wx.hideLoading() + wx.showToast({ + title: '已保存到相册', + icon: 'success' + }) + this.onCloseQrcodeModal() + }, + fail: (err) => { + wx.hideLoading() + if (err.errMsg.includes('auth deny')) { + wx.showModal({ + title: '需要相册权限', + content: '请在设置中允许访问相册', + confirmText: '去设置', + success: (modalRes) => { + if (modalRes.confirm) { + wx.openSetting() + } + } + }) + } else { + wx.showToast({ + title: '保存失败', + icon: 'none' + }) + } + } + }) + } else { + wx.hideLoading() + wx.showToast({ + title: '下载失败', + icon: 'none' + }) + } + }, + fail: () => { + wx.hideLoading() + wx.showToast({ + title: '下载失败', + icon: 'none' + }) + } + }) + }, + + /** + * 点击活动卡片 + */ + onActivityTap(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/activity-detail/activity-detail?id=${id}` + }) + }, + + /** + * 立即报名 + */ + onSignUp(e) { + const id = e.currentTarget.dataset.id + const index = e.currentTarget.dataset.index + const activity = this.data.activityList[index] + + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ + url: '/pages/login/login' + }) + return + } + + // 检查活动状态 + if (activity.status === 'full' || activity.status === 'ended') { + const qrCode = activity.activityGuideQrcode || activity.activity_guide_qrcode || this.data.qrcodeImageUrl || 'https://ai-c.maimanji.com/api/common/qrcode?type=group' + this.setData({ + qrcodeImageUrl: qrCode, + showQrcodeModal: true + }) + return + } + + wx.showModal({ + title: '确认报名', + content: '确定要报名参加这个活动吗?', + success: (res) => { + if (res.confirm) { + this.handleSignUp(id, index) + } + } + }) + }, + + /** + * 处理报名 + */ + async handleSignUp(activityId, index) { + try { + wx.showLoading({ title: '报名中...' }) + + const res = await api.activity.signup(activityId) + + wx.hideLoading() + + if (res.success) { + wx.showToast({ + title: '报名成功', + icon: 'success' + }) + + // 刷新列表 + this.loadActivityList() + } else { + // 检查是否需要显示二维码(后端开关关闭或活动已结束) + if (res.code === 'QR_CODE_REQUIRED' || res.error === 'QR_CODE_REQUIRED' || res.code === 'ACTIVITY_ENDED' || res.error === '活动已结束' || res.code === 'ACTIVITY_FULL' || res.error === '活动已满员') { + const activity = this.data.activityList[index] + if (activity.activityGuideQrcode || activity.activity_guide_qrcode) { + this.setData({ qrcodeImageUrl: activity.activityGuideQrcode || activity.activity_guide_qrcode }) + } + this.setData({ + showQrcodeModal: true + }) + if (res.code === 'ACTIVITY_ENDED' || res.error === '活动已结束' || res.code === 'ACTIVITY_FULL' || res.error === '活动已满员') { + const tip = (res.code === 'ACTIVITY_FULL' || res.error === '活动已满员') ? '活动已满员,进群查看更多' : '活动已结束,进群查看更多' + wx.showToast({ title: tip, icon: 'none' }) + } + } else { + wx.showToast({ + title: res.error || '报名失败', + icon: 'none' + }) + } + } + } catch (err) { + wx.hideLoading() + console.error('报名失败', err) + + // 捕获特定错误码以显示二维码 + const isQrRequired = err && (err.code === 'QR_CODE_REQUIRED' || (err.data && err.data.code === 'QR_CODE_REQUIRED')) + const isActivityEnded = err && (err.code === 'ACTIVITY_ENDED' || (err.data && err.data.code === 'ACTIVITY_ENDED') || err.error === '活动已结束') + const isActivityFull = err && (err.code === 'ACTIVITY_FULL' || (err.data && err.data.code === 'ACTIVITY_FULL') || err.error === '活动已满员') + + if (isQrRequired || isActivityEnded || isActivityFull) { + const activity = this.data.activityList[index] + if (activity.activityGuideQrcode || activity.activity_guide_qrcode) { + this.setData({ qrcodeImageUrl: activity.activityGuideQrcode || activity.activity_guide_qrcode }) + } + this.setData({ + showQrcodeModal: true + }) + if (isActivityEnded || isActivityFull) { + const tip = isActivityFull ? '活动已满员,进群查看更多' : '活动已结束,进群查看更多' + wx.showToast({ title: tip, icon: 'none' }) + } + } else { + wx.showToast({ + title: err.error || err.message || '报名失败', + icon: 'none' + }) + } + } + } +}) diff --git a/pages/city-activities/city-activities.json b/pages/city-activities/city-activities.json new file mode 100644 index 0000000..6e5ee5a --- /dev/null +++ b/pages/city-activities/city-activities.json @@ -0,0 +1,7 @@ +{ + "navigationStyle": "custom", + "navigationBarTextStyle": "black", + "usingComponents": { + "app-icon": "../../components/icon/icon" + } +} diff --git a/pages/city-activities/city-activities.wxml b/pages/city-activities/city-activities.wxml new file mode 100644 index 0000000..e1ee6e9 --- /dev/null +++ b/pages/city-activities/city-activities.wxml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + 同城活动 + + + + + + + + + {{selectedCity}}同城群 + + + {{selectedCity}} + + + + + 点击立即加入 + + + + + + 热门活动 + 共 {{activityList.length}} 个活动 + + + + + + + + + + + + {{item.venue}} + + + + + + {{item.title}} + + + + {{item.date}} + + + + {{item.location}} + + + + + {{item.heat}} + + + + + + + {{item.participants}}人已报名 + + + + + + + + + + 暂无活动 + + + + + + + + + + + + + + + + + + + + + 加入{{selectedCity}}活动群 + + + 及时获取第一手活动资讯 + + + + + + + + + 保存二维码 + + + + diff --git a/pages/city-activities/city-activities.wxss b/pages/city-activities/city-activities.wxss new file mode 100644 index 0000000..643d65d --- /dev/null +++ b/pages/city-activities/city-activities.wxss @@ -0,0 +1,479 @@ +/* 同城活动页面样式 - 玫瑰紫版 v3.0 */ +page { + background: linear-gradient(180deg, #E8C3D4 0%, #F5E6ED 100%); +} + +.page-container { + min-height: 100vh; + background: linear-gradient(180deg, #E8C3D4 0%, #F5E6ED 100%); +} + +/* 固定导航栏容器 */ +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; + background: rgba(248, 249, 252, 0.75); + backdrop-filter: blur(20rpx) saturate(180%); + -webkit-backdrop-filter: blur(20rpx) saturate(180%); +} + +/* 状态栏 */ +.status-bar { + background: transparent; +} + +/* 导航栏 */ +.nav-bar { + display: flex; + align-items: center; + justify-content: center; + padding: 0 32rpx; + background: transparent; + position: relative; +} + +.nav-back { + position: absolute; + left: 32rpx; + top: 50%; + transform: translateY(-50%); + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.back-icon { + width: 48rpx; + height: 48rpx; +} + +.nav-title { + font-size: 36rpx; + font-weight: 700; + color: #1A1A1A; + line-height: 1; +} + +/* 内容滚动区域 */ +.content-scroll { + height: 100vh; + box-sizing: border-box; +} + +/* 同城群推广卡片 - 玫瑰紫渐变 */ +.city-group-card { + margin: 32rpx; + padding: 32rpx 40rpx; + min-height: 128rpx; + background: linear-gradient(135deg, + rgba(232, 195, 212, 0.6) 0%, + rgba(245, 230, 237, 0.6) 100%); + backdrop-filter: blur(16rpx) saturate(150%); + border: 2rpx solid rgba(145, 69, 132, 0.3); + border-radius: 48rpx; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 4rpx 20rpx rgba(145, 69, 132, 0.12); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.city-group-card:active { + transform: scale(0.98); + box-shadow: 0 2rpx 12rpx rgba(145, 69, 132, 0.18); +} + +.group-info { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 16rpx; + padding-right: 24rpx; +} + +.group-title { + font-size: 40rpx; + font-weight: 700; + color: #1A1A1A; + line-height: 1.4; + white-space: nowrap; +} + +/* 城市选择器 */ +.city-selector { + display: flex; + align-items: center; + gap: 12rpx; + padding: 12rpx 28rpx; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(16rpx) saturate(150%); + border-radius: 100rpx; + border: 2rpx solid rgba(145, 69, 132, 0.2); + box-shadow: 0 4rpx 16rpx rgba(145, 69, 132, 0.08), + 0 2rpx 8rpx rgba(145, 69, 132, 0.04); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; +} + +.city-selector:active { + transform: scale(0.96); + box-shadow: 0 2rpx 12rpx rgba(145, 69, 132, 0.12); + background: rgba(255, 255, 255, 1); +} + +.city-name { + font-size: 32rpx; + font-weight: 700; + color: #914584; + white-space: nowrap; +} + +.city-arrow { + width: 24rpx; + height: 24rpx; + opacity: 0.7; +} + +.join-btn { + padding: 0 40rpx; + height: 88rpx; + background: linear-gradient(135deg, #914584 0%, #7A3A6F 100%); + border-radius: 100rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + font-weight: 700; + color: #fff; + white-space: nowrap; + flex-shrink: 0; + box-shadow: 0 6rpx 24rpx rgba(145, 69, 132, 0.4), + 0 3rpx 12rpx rgba(145, 69, 132, 0.3); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; +} + +.join-btn:active { + transform: scale(0.96); + box-shadow: 0 4rpx 16rpx rgba(145, 69, 132, 0.45); +} + +/* 活动列表标题 */ +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 32rpx; + margin-bottom: 32rpx; +} + +.section-title { + font-size: 44rpx; + font-weight: 700; + color: #1A1A1A; +} + +.activity-count { + font-size: 28rpx; + color: #914584; + font-weight: 500; +} + +/* 活动列表 - 毛玻璃卡片 */ +.activity-list { + padding: 0 32rpx; +} + +.activity-card { + margin-bottom: 32rpx; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(16rpx); + border-radius: 32rpx; + overflow: hidden; + box-shadow: 0 8rpx 32rpx rgba(145, 69, 132, 0.12), + 0 4rpx 16rpx rgba(145, 69, 132, 0.08); + border: 1rpx solid rgba(145, 69, 132, 0.15); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.activity-card:active { + transform: scale(0.98); + box-shadow: 0 4rpx 16rpx rgba(145, 69, 132, 0.15); +} + +/* 活动图片容器 */ +.activity-image-container { + position: relative; + width: 100%; + height: 360rpx; + overflow: hidden; + background: linear-gradient(135deg, #F5E6ED 0%, #FAF5F8 100%); +} + +.activity-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.location-badge { + position: absolute; + top: 24rpx; + left: 24rpx; + padding: 12rpx 24rpx; + background: rgba(122, 58, 111, 0.85); + backdrop-filter: blur(12rpx); + border-radius: 100rpx; + display: flex; + align-items: center; + gap: 8rpx; + font-size: 24rpx; + color: #FFFFFF; + font-weight: 500; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.15); +} + +.location-icon { + width: 24rpx; + height: 24rpx; +} + +/* 活动信息 */ +.activity-info { + padding: 40rpx; +} + +.activity-title { + font-size: 36rpx; + font-weight: 700; + color: #7A3A6F; + margin-bottom: 20rpx; + line-height: 1.4; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; +} + +.activity-meta { + display: flex; + align-items: center; + gap: 32rpx; + margin-bottom: 24rpx; +} + +.meta-item { + display: flex; + align-items: center; + gap: 8rpx; + font-size: 26rpx; + color: #914584; +} + +.meta-icon { + width: 28rpx; + height: 28rpx; +} + +/* 热度显示样式 */ +.heat-item { + margin-left: auto; +} + +.heat-text { + color: #F97316; + font-weight: 700; +} + +/* 活动底部 */ +.activity-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 24rpx; + border-top: 1rpx solid rgba(145, 69, 132, 0.1); +} + +.heat-info { + display: flex; + align-items: center; + gap: 8rpx; + font-size: 26rpx; + color: #B39DDB; +} + +.heat-icon { + width: 28rpx; + height: 28rpx; +} + +.signup-btn { + width: 220rpx; + height: 72rpx; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #914584 0%, #7A3A6F 100%); + border-radius: 100rpx; + font-size: 28rpx; + font-weight: 700; + color: #FFFFFF; + box-shadow: 0 6rpx 20rpx rgba(145, 69, 132, 0.3); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + white-space: nowrap; + flex-shrink: 0; +} + +.signup-btn:active { + transform: scale(0.95); + box-shadow: 0 4rpx 12rpx rgba(145, 69, 132, 0.35); +} + +/* 空状态 */ +.empty-state { + padding: 120rpx 32rpx; + text-align: center; +} + +.empty-icon { + width: 200rpx; + height: 200rpx; + margin: 0 auto 32rpx; + opacity: 0.5; +} + +.empty-text { + font-size: 28rpx; + color: #B39DDB; +} + +/* 二维码弹窗 */ +.qrcode-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; +} + +/* 遮罩层 */ +.modal-mask { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4rpx); +} + +/* 弹窗内容 */ +.modal-content { + position: relative; + width: 680rpx; + background: #FFFFFF; + border-radius: 64rpx; + padding: 64rpx; + box-shadow: 0 50rpx 100rpx -24rpx rgba(0, 0, 0, 0.25); + display: flex; + flex-direction: column; + align-items: center; + z-index: 1; +} + +/* 关闭按钮 */ +.close-btn { + position: absolute; + top: 32rpx; + right: 32rpx; + width: 72rpx; + height: 72rpx; + background: #F1F5F9; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.close-btn:active { + transform: scale(0.9); + background: #E2E8F0; +} + +.close-icon { + width: 40rpx; + height: 40rpx; +} + +/* 标题 */ +.modal-title { + font-size: 48rpx; + font-weight: 700; + color: #1D293D; + text-align: center; + margin-bottom: 16rpx; + line-height: 1.5; +} + +/* 副标题 */ +.modal-subtitle { + font-size: 32rpx; + color: #62748E; + text-align: center; + margin-bottom: 48rpx; + line-height: 1.5; +} + +/* 二维码容器 */ +.qrcode-container { + width: 440rpx; + height: 440rpx; + background: #F8FAFC; + border: 2rpx solid #F1F5F9; + border-radius: 40rpx; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 48rpx; + overflow: hidden; +} + +.qrcode-image { + width: 404rpx; + height: 404rpx; + border-radius: 24rpx; +} + +/* 保存按钮 */ +.save-btn { + width: 552rpx; + height: 116rpx; + background: #07C160; + border-radius: 100rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 40rpx; + font-weight: 700; + color: #FFFFFF; + box-shadow: 0 20rpx 30rpx -6rpx rgba(220, 252, 231, 1), + 0 8rpx 12rpx -8rpx rgba(220, 252, 231, 1); + transition: all 0.3s ease; +} + +.save-btn:active { + transform: scale(0.96); + box-shadow: 0 10rpx 20rpx -6rpx rgba(220, 252, 231, 1); +} diff --git a/pages/city-selector/city-selector.js b/pages/city-selector/city-selector.js new file mode 100644 index 0000000..ae4ee7b --- /dev/null +++ b/pages/city-selector/city-selector.js @@ -0,0 +1,151 @@ +// pages/city-selector/city-selector.js - 城市选择页面 + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + + // 当前选中的城市 + selectedCity: '', + + // 热门城市(一线城市) + hotCities: [ + '北京市', '上海市', '广州市', '深圳市', '杭州市', '长沙市' + ], + + // 全国主要城市(按首字母分组) + cityGroups: [ + { + letter: 'A', + cities: ['鞍山市', '安庆市', '安阳市', '安顺市'] + }, + { + letter: 'B', + cities: ['北京市', '保定市', '包头市', '蚌埠市', '宝鸡市', '本溪市', '滨州市'] + }, + { + letter: 'C', + cities: ['重庆市', '成都市', '长沙市', '长春市', '常州市', '沧州市', '承德市', '赤峰市', '潮州市', '郴州市', '滁州市', '常德市'] + }, + { + letter: 'D', + cities: ['大连市', '东莞市', '大庆市', '大同市', '丹东市', '德州市', '东营市', '德阳市', '达州市'] + }, + { + letter: 'F', + cities: ['佛山市', '福州市', '抚顺市', '阜阳市', '抚州市', '防城港市'] + }, + { + letter: 'G', + cities: ['广州市', '贵阳市', '桂林市', '赣州市', '广元市', '贵港市'] + }, + { + letter: 'H', + cities: ['杭州市', '哈尔滨市', '合肥市', '海口市', '呼和浩特市', '惠州市', '邯郸市', '衡阳市', '淮安市', '湖州市', '葫芦岛市', '淮南市', '黄石市', '菏泽市', '衡水市', '淮北市', '黄冈市', '怀化市', '鹤壁市', '河源市', '贺州市'] + }, + { + letter: 'J', + cities: ['济南市', '济宁市', '吉林市', '锦州市', '金华市', '嘉兴市', '江门市', '九江市', '焦作市', '荆州市', '吉安市', '揭阳市', '景德镇市', '晋中市', '晋城市', '荆门市', '鸡西市', '佳木斯市'] + }, + { + letter: 'K', + cities: ['昆明市', '开封市'] + }, + { + letter: 'L', + cities: ['兰州市', '洛阳市', '廊坊市', '临沂市', '柳州市', '连云港市', '聊城市', '泸州市', '漯河市', '娄底市', '六安市', '龙岩市', '莱芜市', '辽阳市', '丽水市', '六盘水市', '辽源市', '来宾市', '临汾市', '吕梁市'] + }, + { + letter: 'M', + cities: ['绵阳市', '茂名市', '梅州市', '马鞍山市', '牡丹江市', '眉山市'] + }, + { + letter: 'N', + cities: ['南京市', '宁波市', '南昌市', '南宁市', '南通市', '南阳市', '南充市', '宁德市', '内江市', '南平市'] + }, + { + letter: 'P', + cities: ['平顶山市', '盘锦市', '莆田市', '萍乡市', '濮阳市', '攀枝花市'] + }, + { + letter: 'Q', + cities: ['青岛市', '泉州市', '秦皇岛市', '齐齐哈尔市', '清远市', '曲靖市', '衢州市', '钦州市', '庆阳市'] + }, + { + letter: 'R', + cities: ['日照市'] + }, + { + letter: 'S', + cities: ['上海市', '深圳市', '苏州市', '沈阳市', '石家庄市', '汕头市', '绍兴市', '三亚市', '宿迁市', '商丘市', '十堰市', '韶关市', '遂宁市', '宿州市', '邵阳市', '上饶市', '汕尾市', '三明市', '朔州市', '四平市', '松原市', '随州市', '绥化市', '双鸭山市', '石嘴山市'] + }, + { + letter: 'T', + cities: ['天津市', '太原市', '唐山市', '泰安市', '台州市', '泰州市', '铁岭市', '通辽市', '通化市', '铜陵市', '铜川市', '铜仁市', '天水市'] + }, + { + letter: 'W', + cities: ['武汉市', '无锡市', '温州市', '潍坊市', '芜湖市', '威海市', '乌鲁木齐市', '梧州市', '渭南市', '乌海市', '乌兰察布市'] + }, + { + letter: 'X', + cities: ['西安市', '厦门市', '徐州市', '襄阳市', '新乡市', '湘潭市', '许昌市', '信阳市', '咸阳市', '孝感市', '邢台市', '咸宁市', '宣城市', '忻州市', '西宁市', '湘西土家族苗族自治州'] + }, + { + letter: 'Y', + cities: ['烟台市', '扬州市', '宜昌市', '盐城市', '银川市', '岳阳市', '运城市', '榆林市', '宜宾市', '阳江市', '玉林市', '宜春市', '营口市', '益阳市', '永州市', '玉溪市', '延安市', '鹰潭市', '伊春市', '云浮市', '阳泉市', '延边朝鲜族自治州'] + }, + { + letter: 'Z', + cities: ['郑州市', '珠海市', '中山市', '淄博市', '株洲市', '镇江市', '湛江市', '漳州市', '遵义市', '舟山市', '枣庄市', '张家口市', '周口市', '驻马店市', '肇庆市', '自贡市', '资阳市', '张家界市', '昭通市', '中卫市', '张掖市'] + } + ] + }, + + onLoad(options) { + // 获取导航栏高度 + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight, + selectedCity: options.current || '深圳市' + }) + }, + + /** + * 返回上一页 + */ + onBack() { + wx.navigateBack() + }, + + /** + * 选择城市 + */ + onSelectCity(e) { + const city = e.currentTarget.dataset.city + + // 获取上一页面 + const pages = getCurrentPages() + const prevPage = pages[pages.length - 2] + + // 更新上一页面的城市数据 + if (prevPage) { + prevPage.setData({ selectedCity: city }) + // 如果上一页面有loadActivityList方法,调用它重新加载数据 + if (typeof prevPage.loadActivityList === 'function') { + prevPage.loadActivityList() + } + } + + // 返回上一页 + wx.navigateBack() + } +}) diff --git a/pages/city-selector/city-selector.json b/pages/city-selector/city-selector.json new file mode 100644 index 0000000..b16ef3e --- /dev/null +++ b/pages/city-selector/city-selector.json @@ -0,0 +1,4 @@ +{ + "navigationStyle": "custom", + "usingComponents": {} +} diff --git a/pages/city-selector/city-selector.wxml b/pages/city-selector/city-selector.wxml new file mode 100644 index 0000000..d8b4a1e --- /dev/null +++ b/pages/city-selector/city-selector.wxml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + 选择城市 + + + + + + + + 热门城市 + + + {{item}} + + + + + + + 全部城市 + + + {{item.letter}} + + + {{city}} + + + + + + + diff --git a/pages/city-selector/city-selector.wxss b/pages/city-selector/city-selector.wxss new file mode 100644 index 0000000..dedf000 --- /dev/null +++ b/pages/city-selector/city-selector.wxss @@ -0,0 +1,137 @@ +/* pages/city-selector/city-selector.wxss */ + +.page { + min-height: 100vh; + background: linear-gradient(180deg, #F8F5FF 0%, #FFF5F8 100%); +} + +/* ========== 固定导航栏 ========== */ +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; + background: rgba(242, 237, 255, 0.6); + backdrop-filter: blur(10px); +} + +.status-bar { + background: transparent; +} + +.nav-bar { + display: flex; + align-items: center; + justify-content: center; + padding: 0 32rpx; + background: transparent; + position: relative; +} + +.nav-back { + position: absolute; + left: 32rpx; + top: 50%; + transform: translateY(-50%); + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.back-icon { + width: 48rpx; + height: 48rpx; +} + +.nav-title { + font-size: 36rpx; + font-weight: 700; + color: #1F2937; + line-height: 1; +} + +/* ========== 内容滚动区域 ========== */ +.content-scroll { + height: 100vh; + box-sizing: border-box; +} + +/* ========== 分区 ========== */ +.section { + padding: 32rpx; +} + +.section-title { + font-size: 32rpx; + font-weight: 700; + color: #1F2937; + margin-bottom: 24rpx; +} + +/* ========== 热门城市 ========== */ +.hot-cities { + display: flex; + flex-wrap: wrap; + gap: 24rpx; +} + +.city-tag { + padding: 20rpx 40rpx; + background: rgba(255, 255, 255, 0.8); + border-radius: 16rpx; + font-size: 28rpx; + color: #4B5563; + border: 2rpx solid transparent; + transition: all 0.2s; +} + +.city-tag.active { + background: linear-gradient(135deg, #A78BFA 0%, #EC4899 100%); + color: #FFFFFF; + font-weight: 600; + border-color: transparent; +} + +/* ========== 城市分组 ========== */ +.city-groups { + margin-top: 16rpx; +} + +.city-group { + margin-bottom: 32rpx; +} + +.group-letter { + font-size: 28rpx; + font-weight: 700; + color: #A78BFA; + margin-bottom: 16rpx; + padding-left: 8rpx; +} + +.group-cities { + display: flex; + flex-wrap: wrap; + gap: 16rpx; +} + +.city-item { + padding: 16rpx 32rpx; + background: rgba(255, 255, 255, 0.6); + border-radius: 12rpx; + font-size: 26rpx; + color: #4B5563; + border: 2rpx solid transparent; + transition: all 0.2s; +} + +.city-item.active { + background: linear-gradient(135deg, #A78BFA 0%, #EC4899 100%); + color: #FFFFFF; + font-weight: 600; + border-color: transparent; +} diff --git a/pages/commission/commission.js b/pages/commission/commission.js new file mode 100644 index 0000000..d13571f --- /dev/null +++ b/pages/commission/commission.js @@ -0,0 +1,666 @@ +// pages/commission/commission.js - 佣金明细页面 +// 对接后端API + +const api = require('../../utils/api') +const errorHandler = require('../../utils/errorHandler') + +// 缓存配置 +const CACHE_CONFIG = { + STATS_KEY: 'commission_stats_cache', + RECORDS_KEY: 'commission_records_cache', + EXPIRE_TIME: 5 * 60 * 1000 // 5分钟有效期 +} + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + navHeight: 96, // Keep for compatibility if used elsewhere + loading: false, + currentTab: 'all', + // 佣金统计 + isDistributor: false, + referralCode: '', + cardType: '', + commissionBalance: '0.00', + totalCommission: '0.00', + pendingAmount: '0.00', // Confirmed field for display + totalWithdrawn: '0.00', + totalReferrals: 0, + totalContribution: '0.00', + // 佣金比例 + rechargeRate: 10, + vipRate: 15, + cardRate: 20, + // 兼容旧字段 + totalEarnings: '0.00', + availableBalance: '0.00', + withdrawnAmount: '0.00', + // 记录列表 + allList: [], + commissionList: [], + page: 1, + hasMore: true, + // 缓存状态 + cacheExpired: false, + lastUpdateTime: '', + defaultAvatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=500&auto=format&fit=crop&q=60', + cardTitle: '守护会员', + pickerDate: '', // YYYY-MM + pickerDateDisplay: '' // YYYY年MM月 + }, + + onLoad() { + // Init Date + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + + this.setData({ + pickerDate: `${year}-${month}`, + pickerDateDisplay: `${year}年${month}月` + }); + + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const navHeight = statusBarHeight + 48 + + this.setData({ + statusBarHeight, + navHeight + }) + + // 先尝试加载缓存(快速显示) + this.loadFromCache() + + // 然后加载最新数据(确保数据准确) + this.loadCommissionStats() + this.loadCommissionRecords() + }, + + /** + * 页面显示时刷新数据 + */ + onShow() { + // 每次显示页面时,强制刷新用户信息 + // 确保从其他页面返回时数据是最新的 + this.loadCommissionStats() + }, + + /** + * 从缓存加载数据 + */ + loadFromCache() { + try { + // 加载统计数据缓存 + const statsCache = wx.getStorageSync(CACHE_CONFIG.STATS_KEY) + if (statsCache && this.isCacheValid(statsCache.timestamp)) { + const cachedData = statsCache.data || {} + // 强制修正缓存中的错误规定值 + if (cachedData.pendingAmount === '210.00' || cachedData.pendingAmount === 210) { + cachedData.pendingAmount = '0.00' + } + this.setData({ + ...cachedData, + cacheExpired: false, + lastUpdateTime: this.formatCacheTime(statsCache.timestamp) + }) + } else { + this.setData({ cacheExpired: true }) + } + + // 加载记录列表缓存 + const recordsCache = wx.getStorageSync(CACHE_CONFIG.RECORDS_KEY) + if (recordsCache && this.isCacheValid(recordsCache.timestamp)) { + this.setData({ + allList: recordsCache.data, + commissionList: recordsCache.data + }) + } + } catch (error) { + console.error('加载缓存失败:', error) + } + }, + + /** + * 检查缓存是否有效 + */ + isCacheValid(timestamp) { + if (!timestamp) return false + const now = Date.now() + return (now - timestamp) < CACHE_CONFIG.EXPIRE_TIME + }, + + /** + * 格式化缓存时间 + */ + formatCacheTime(timestamp) { + const date = new Date(timestamp) + const hour = String(date.getHours()).padStart(2, '0') + const minute = String(date.getMinutes()).padStart(2, '0') + return `${hour}:${minute}` + }, + + /** + * 保存统计数据到缓存 + */ + saveStatsToCache(data) { + try { + wx.setStorageSync(CACHE_CONFIG.STATS_KEY, { + data, + timestamp: Date.now() + }) + } catch (error) { + console.error('保存统计缓存失败:', error) + } + }, + + /** + * 保存记录列表到缓存 + */ + saveRecordsToCache(records) { + try { + wx.setStorageSync(CACHE_CONFIG.RECORDS_KEY, { + data: records, + timestamp: Date.now() + }) + } catch (error) { + console.error('保存记录缓存失败:', error) + } + }, + + /** + * 清除缓存 + */ + clearCache() { + try { + wx.removeStorageSync(CACHE_CONFIG.STATS_KEY) + wx.removeStorageSync(CACHE_CONFIG.RECORDS_KEY) + this.setData({ cacheExpired: true, lastUpdateTime: '' }) + wx.showToast({ + title: '缓存已清除', + icon: 'success' + }) + } catch (error) { + console.error('清除缓存失败:', error) + } + }, + + /** + * 下拉刷新 + */ + onPullDownRefresh() { + // 清除缓存,强制刷新 + this.clearCache() + + Promise.all([ + this.loadCommissionStats(), + this.loadCommissionRecords() + ]).then(() => { + wx.stopPullDownRefresh() + wx.showToast({ + title: '刷新成功', + icon: 'success' + }) + }) + }, + + /** + * 上拉加载更多 + */ + onReachBottom() { + if (this.data.hasMore && !this.data.loading) { + this.loadMoreRecords() + } + }, + + /** + * 加载佣金统计 + */ + async loadCommissionStats() { + try { + const res = await api.commission.getStats() + + if (res.success && res.data) { + const data = res.data + const statsData = { + isDistributor: data.isDistributor || false, + referralCode: data.referralCode || '', + cardType: data.cardType || '', + commissionBalance: Number(data.commissionBalance || 0).toFixed(2), + totalCommission: Number(data.totalCommission || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }), + pendingAmount: Number(data.pendingWithdrawal || data.pendingAmount || 0).toFixed(2), + totalWithdrawn: Number(data.totalWithdrawn || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }), + totalReferrals: data.totalReferrals || 0, + totalContribution: Number(data.totalContribution || data.total_contribution || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }), + pendingWithdrawal: Number(data.pendingWithdrawal || 0).toFixed(2), + rechargeRate: data.rechargeRate || 10, + vipRate: data.vipRate || 15, + cardRate: data.cardRate || 20, + // 兼容旧字段 + totalEarnings: Number(data.totalCommission || data.total_earnings || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }), + availableBalance: Number(data.commissionBalance || data.available_balance || 0).toFixed(2), + withdrawnAmount: Number(data.totalWithdrawn || data.withdrawn_amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) + } + + this.setData({ + ...statsData, + cardTitle: this.getCardTitle(statsData.cardType), + cacheExpired: false, + lastUpdateTime: this.formatCacheTime(Date.now()) + }) + + // 保存到缓存 + this.saveStatsToCache(statsData) + + // 如果不是分销商,显示引导提示 + if (!data.isDistributor) { + this.showBecomeDistributorTip() + } + } + } catch (err) { + console.log('获取佣金统计失败', err) + // 如果有缓存,显示缓存失效提示 + if (!this.data.cacheExpired) { + wx.showToast({ + title: '数据加载失败,显示缓存数据', + icon: 'none', + duration: 2000 + }) + } + } + }, + + /** + * 显示成为分销商提示 + */ + showBecomeDistributorTip() { + wx.showModal({ + title: '成为分销商', + content: '购买身份卡即可成为分销商,推荐好友赚佣金!', + confirmText: '了解详情', + cancelText: '暂不需要', + success: (res) => { + if (res.confirm) { + // 跳转到推广页面查看详情 + wx.navigateTo({ + url: '/pages/promote/promote' + }) + } + } + }) + }, + + /** + * 加载佣金记录 + */ + async loadCommissionRecords() { + this.setData({ loading: true, page: 1 }) + + try { + const res = await api.commission.getRecords({ page: 1, limit: 20 }) + + if (res.success && res.data) { + const records = (res.data.list || res.data || []).map(record => this.transformRecord(record)) + + this.setData({ + allList: records, + commissionList: records, + hasMore: records.length >= 20, + loading: false + }) + + // 保存到缓存 + this.saveRecordsToCache(records) + } else { + this.setData({ allList: [], commissionList: [], loading: false }) + } + } catch (err) { + console.error('加载佣金记录失败', err) + this.setData({ loading: false }) + + // 如果有缓存,显示缓存失效提示 + if (this.data.allList.length > 0) { + wx.showToast({ + title: '数据加载失败,显示缓存数据', + icon: 'none', + duration: 2000 + }) + } + } + }, + + /** + * 加载更多记录 + */ + async loadMoreRecords() { + const nextPage = this.data.page + 1 + this.setData({ loading: true }) + + try { + const res = await api.commission.getRecords({ page: nextPage, limit: 20 }) + + if (res.success && res.data) { + const newRecords = (res.data.list || res.data || []).map(record => this.transformRecord(record)) + const allList = [...this.data.allList, ...newRecords] + + this.setData({ + allList, + page: nextPage, + hasMore: newRecords.length >= 20, + loading: false + }) + + // 更新当前筛选列表 + this.filterRecords(this.data.currentTab) + } else { + this.setData({ hasMore: false, loading: false }) + } + } catch (err) { + console.error('加载更多记录失败', err) + this.setData({ loading: false }) + } + }, + + /** + * 转换记录数据格式 + */ + transformRecord(record) { + let titleText = record.fromUserName || record.userName || '用户'; + let descText = 'VIP月卡'; + if (record.amount > 100) descText = 'SVIP年卡'; + + if (record.fromUserLevel) { + descText = this.getUserLevelText(record.fromUserLevel); + } else if (record.orderType === 'vip' || record.orderType === 'svip') { + descText = record.orderType.toUpperCase() === 'SVIP' ? 'SVIP会员' : 'VIP会员'; + } else if (record.orderType === 'identity_card') { + descText = '身份会员'; + } else if (record.orderType === 'companion_chat') { + descText = '陪伴聊天'; + } + + const dateObj = new Date(record.created_at || record.createdAt); + const mm = String(dateObj.getMonth() + 1).padStart(2, '0'); + const dd = String(dateObj.getDate()).padStart(2, '0'); + const hh = String(dateObj.getHours()).padStart(2, '0'); + const min = String(dateObj.getMinutes()).padStart(2, '0'); + const fmtTime = `${mm}-${dd} ${hh}:${min}`; + + return { + id: record.id, + type: record.type, + title: titleText, + desc: descText, + amount: record.commissionAmount ? record.commissionAmount.toFixed(0) : (record.amount ? Number(record.amount).toFixed(0) : '0'), + status: record.status || 'settled', + statusText: record.status === 'pending' ? '待结算' : '已结算', + time: this.formatTime(record.created_at || record.createdAt), + orderNo: record.orderId ? `ORD${record.orderId.substring(0, 12)}` : 'ORD2024012401', + userAvatar: record.userAvatar || record.fromUserAvatar || record.avatar || '', + listTitle: titleText, + fmtTime: fmtTime, + } + }, + + getUserLevelText(level) { + const levelMap = { + 'vip': 'VIP会员', + 'svip': 'SVIP会员', + 'guardian': '守护会员', + 'companion': '陪伴会员', + 'partner': '城市合伙人', + '1': '普通用户', + '2': 'VIP会员', + '3': 'SVIP会员', + '4': '守护会员', + '5': '陪伴会员', + '6': '城市合伙人' + }; + return levelMap[level] || levelMap['1']; + }, + + getCardTitle(type) { + const map = { + 'guardian_card': '守护会员', + 'companion_card': '陪伴会员', + 'identity_card': '身份会员', + 'vip': 'VIP会员', + 'partner': '城市合伙人' + }; + return map[type] || '守护会员'; + }, + + goToTeam() { + wx.navigateTo({ + url: '/pages/team/team', + }); + }, + + /** + * 格式化时间 + */ + formatTime(dateStr) { + if (!dateStr) return '' + const date = new Date(dateStr) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hour = String(date.getHours()).padStart(2, '0') + const minute = String(date.getMinutes()).padStart(2, '0') + return `${year}-${month}-${day} ${hour}:${minute}` + }, + + onBack() { + wx.navigateBack() + }, + + onTabChange(e) { + const tab = e.currentTarget.dataset.tab + this.setData({ currentTab: tab }) + this.filterRecords(tab) + }, + + /** + * 筛选记录 + */ + filterRecords(tab) { + let list = [] + + if (tab === 'all') { + list = this.data.allList + } else if (tab === 'settled') { + list = this.data.allList.filter(item => item.status === 'settled') + } else if (tab === 'pending') { + list = this.data.allList.filter(item => item.status === 'pending') + } + + this.setData({ commissionList: list }) + }, + + onDateChange(e) { + const val = e.detail.value; // YYYY-MM + const [y, m] = val.split('-'); + this.setData({ + pickerDate: val, + pickerDateDisplay: `${y}年${m}月` + }); + // TODO: Call API to reload records with date filter if needed + this.loadCommissionRecords(); // Reload with new date + }, + + /** + * 跳转到提现页面 + */ + onWithdraw() { + wx.navigateTo({ + url: '/pages/withdraw/withdraw' + }) + }, + + /** + * 跳转到推广页面 + */ + onPromote() { + wx.navigateTo({ + url: '/pages/promote/promote' + }) + }, + + /** + * 跳转到推荐用户列表 + */ + async goToReferrals() { + // 显示加载提示 + wx.showLoading({ title: '加载中...', mask: true }) + + try { + // 强制刷新用户信息,确保使用最新数据 + const res = await api.commission.getStats() + + if (res.success && res.data) { + const isDistributor = res.data.isDistributor || false + + // 更新本地状态 + this.setData({ isDistributor }) + + wx.hideLoading() + + // 检查是否是分销商 + if (!isDistributor) { + wx.showModal({ + title: '成为分销商', + content: '购买身份卡即可成为分销商,推荐好友赚佣金!', + confirmText: '了解详情', + cancelText: '取消', + success: (modalRes) => { + if (modalRes.confirm) { + wx.navigateTo({ + url: '/pages/promote/promote' + }) + } + } + }) + return + } + + // 跳转到推荐用户列表 + wx.navigateTo({ + url: '/pages/referrals/referrals' + }) + } else { + wx.hideLoading() + wx.showToast({ + title: '获取用户信息失败', + icon: 'none' + }) + } + } catch (error) { + wx.hideLoading() + console.error('获取用户信息失败:', error) + wx.showToast({ + title: '网络错误,请重试', + icon: 'none' + }) + } + }, + + /** + * 跳转到提现记录 + */ + goToWithdrawRecords() { + wx.navigateTo({ + url: '/pages/withdraw-records/withdraw-records' + }) + }, + + /** + * 微信分享推荐码 + */ + onShareAppMessage() { + const { referralCode, isDistributor } = this.data + + if (!isDistributor || !referralCode) { + return { + title: '心伴AI - 情感陪伴聊天机器人', + path: '/pages/index/index', + imageUrl: '/images/share-cover.jpg' + } + } + + return { + title: `我的推荐码:${referralCode},注册即可享受优惠!`, + path: `/pages/index/index?referralCode=${referralCode}`, + imageUrl: '/images/share-commission.png' + } + }, + + /** + * 分享到朋友圈 + */ + onShareTimeline() { + const { referralCode, isDistributor } = this.data + + if (!isDistributor || !referralCode) { + return { + title: '心伴AI - 情感陪伴聊天机器人', + imageUrl: '/images/share-cover.jpg' + } + } + + return { + title: `我的推荐码:${referralCode},注册即可享受优惠!`, + query: `referralCode=${referralCode}`, + imageUrl: '/images/share-commission.png' + } + }, + + /** + * 复制推荐码 + */ + copyReferralCode() { + const { referralCode } = this.data + if (!referralCode) { + wx.showToast({ title: '暂无推荐码', icon: 'none' }) + return + } + + wx.setClipboardData({ + data: referralCode, + success: () => { + wx.showToast({ title: '已复制推荐码', icon: 'success' }) + } + }) + }, + + /** + * 绑定推荐码 + */ + bindReferralCode() { + wx.showModal({ + title: '绑定推荐码', + editable: true, + placeholderText: '请输入推荐码', + success: async (res) => { + if (res.confirm && res.content) { + try { + wx.showLoading({ title: '绑定中...' }) + const result = await api.commission.bindReferral(res.content.trim()) + wx.hideLoading() + + if (result.success) { + wx.showToast({ title: '绑定成功', icon: 'success' }) + this.loadCommissionStats() + } else { + wx.showToast({ title: result.message || '绑定失败', icon: 'none' }) + } + } catch (err) { + wx.hideLoading() + wx.showToast({ title: err.message || '绑定失败', icon: 'none' }) + } + } + } + }) + } +}) diff --git a/pages/commission/commission.json b/pages/commission/commission.json new file mode 100644 index 0000000..d6cb08e --- /dev/null +++ b/pages/commission/commission.json @@ -0,0 +1,6 @@ +{ + "usingComponents": { + "app-icon": "/components/icon/icon" + }, + "navigationStyle": "custom" +} diff --git a/pages/commission/commission.wxml b/pages/commission/commission.wxml new file mode 100644 index 0000000..f73447a --- /dev/null +++ b/pages/commission/commission.wxml @@ -0,0 +1,88 @@ + + + + + + + + 佣金明细 + + + + + + + + + 我的账户 + + + 可提现金额 (元) + {{commissionBalance}} + + + + + + + 待结算 ¥ {{pendingAmount}} + + + + + + 累计结算 ¥ {{totalCommission}} + + + + + + + + 佣金记录 + + + + + + 全部 + + + 已结算 + + + 待结算 + + + + + + 加载中... + 暂无记录 + + + + + + + + {{item.title}} + {{item.desc}} + + + + ¥{{item.amount}} + + {{item.statusText}} + + + + + {{item.time}} + {{item.orderNo}} + + + + 已显示全部数据 + + diff --git a/pages/commission/commission.wxss b/pages/commission/commission.wxss new file mode 100644 index 0000000..f585019 --- /dev/null +++ b/pages/commission/commission.wxss @@ -0,0 +1,266 @@ +.page { + min-height: 100vh; + background: #F8F9FC; + display: flex; + flex-direction: column; +} + +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; +} + +.status-bar { background: transparent; } + +.nav-bar { + display: flex; + align-items: center; + justify-content: center; + padding: 0 32rpx; + position: relative; +} + +.nav-back { + position: absolute; + left: 32rpx; + top: 50%; + transform: translateY(-50%); + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.back-icon { + width: 48rpx; + height: 48rpx; +} + +.nav-title { + font-size: 34rpx; + font-weight: 700; + color: #1A1A1A; +} + +/* Top Card Section */ +.top-section { + padding: 20rpx 32rpx; + background: #F8F9FC; +} + +.commission-card { + background: linear-gradient(135deg, #CF91D3 0%, #B06AB3 100%); + border-radius: 40rpx; + padding: 40rpx 48rpx; + color: #FFFFFF; + box-shadow: 0 10rpx 30rpx rgba(176, 106, 179, 0.3); +} + +.card-header { + display: flex; + align-items: center; + gap: 16rpx; + margin-bottom: 32rpx; +} + +.card-title { + font-size: 32rpx; + font-weight: 600; +} + +.balance-label { + display: block; + font-size: 26rpx; + opacity: 0.9; + margin-bottom: 8rpx; +} + +.balance-value { + font-size: 80rpx; + font-weight: 800; + line-height: 1; + margin-bottom: 40rpx; + display: block; +} + +.divider { + height: 1rpx; + background: rgba(255,255,255,0.2); + margin-bottom: 24rpx; +} + +.stats-row { + display: flex; + align-items: center; +} + +.stat-item { + display: flex; + align-items: center; + gap: 12rpx; +} + +.stat-text { + font-size: 28rpx; + font-weight: 500; +} + +/* List Section */ +.record-title-row { + padding: 20rpx 32rpx 10rpx; +} + +.record-title { + font-size: 34rpx; + font-weight: 800; + color: #111827; +} + +/* Tabs */ +.tabs-container { + display: flex; + align-items: center; + gap: 20rpx; + padding: 20rpx 32rpx; +} + +.tab-item { + padding: 16rpx 48rpx; + border-radius: 40rpx; + font-size: 28rpx; + color: #6B7280; + font-weight: 500; +} + +.tab-item.active { + background: #B06AB3; + color: #FFFFFF; + font-weight: 600; + box-shadow: 0 4rpx 12rpx rgba(176, 106, 179, 0.3); +} + +.tab-item.active-text { + color: #111827; + font-weight: 700; +} + +/* Record List */ +.record-list { + padding: 10rpx 32rpx; +} + +.record-card { + background: #FFFFFF; + border-radius: 32rpx; + padding: 32rpx; + margin-bottom: 24rpx; + box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.02); +} + +.card-top { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24rpx; +} + +.left-info { + display: flex; + align-items: center; +} + +.user-avatar { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + margin-right: 20rpx; +} + +.text-info { + display: flex; + flex-direction: column; + gap: 4rpx; + max-width: 280rpx; +} + +.user-name { + font-size: 30rpx; + font-weight: 700; + color: #111827; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.user-role { + font-size: 24rpx; + color: #9CA3AF; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.right-info { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 8rpx; +} + +.amount-val { + font-size: 36rpx; + font-weight: 800; + color: #B06AB3; +} + +.status-badge { + padding: 4rpx 12rpx; + border-radius: 8rpx; + font-size: 22rpx; +} + +.status-badge.green { + background: #DCFCE7; + color: #16A34A; +} + +.status-badge.gray { + background: #F3F4F6; + color: #9CA3AF; +} + +.card-bottom { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 24rpx; + border-top: 1rpx solid #F9FAFB; +} + +.record-time { + font-size: 24rpx; + color: #9CA3AF; +} + +.record-id { + font-size: 22rpx; + color: #D1D5DB; +} + +.footer-tip { + text-align: center; + color: #D1D5DB; + font-size: 24rpx; + padding: 40rpx 0; +} + +.loading, .empty { + text-align: center; + padding: 60rpx; + color: #9CA3AF; +} diff --git a/pages/companion-apply/ai.code-workspace b/pages/companion-apply/ai.code-workspace new file mode 100644 index 0000000..8e9ff67 --- /dev/null +++ b/pages/companion-apply/ai.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "../../../.." + }, + { + "path": "../../../../../ai-c.maimanji.com_fFGTY" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/pages/companion-apply/companion-apply.js b/pages/companion-apply/companion-apply.js new file mode 100644 index 0000000..c3aa3b8 --- /dev/null +++ b/pages/companion-apply/companion-apply.js @@ -0,0 +1,220 @@ +// pages/companion-apply/companion-apply.js +// 陪聊师申请页面 - 根据 Figma 设计重构 +const api = require('../../utils/api') + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + applyStatus: 'none', // none, pending, approved, rejected + statusTitle: '', + statusDesc: '', + showForm: true, + agreed: false, + formData: { + realName: '', + city: '', + phone: '', + remarks: '' + }, + canSubmit: false + }, + + onLoad() { + // 获取系统信息设置导航栏高度 + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight + }) + + this.checkApplyStatus() + }, + + // 返回上一页 + goBack() { + wx.navigateBack() + }, + + // 检查申请状态 + async checkApplyStatus() { + // 先检查是否已登录(通过 Token 判断) + const token = wx.getStorageSync('auth_token') + console.log('checkApplyStatus - token:', token ? '已登录' : '未登录') + + if (!token) { + console.log('用户未登录,显示申请表单') + this.setData({ applyStatus: 'none' }) + return + } + + wx.showLoading({ title: '加载中...' }) + try { + const res = await api.companion.getApplyStatus() + console.log('获取申请状态响应:', res) + + // 兼容两种响应格式: { success: true, data } 或 { code: 0, data } + const isSuccess = res.success || res.code === 0 + + if (isSuccess && res.data) { + const data = res.data + + // 使用新的状态标识字段 + if (data.isApproved) { + this.setData({ + applyStatus: 'approved', + statusTitle: '您的资料已审核通过', + statusDesc: '恭喜您成为陪聊师!现在可以进入工作台开始接单了' + }) + } else if (data.isPending) { + this.setData({ + applyStatus: 'pending', + statusTitle: '正在审核中', + statusDesc: '您已提交陪聊师申请,正在审核中,请耐心等待' + }) + } else if (data.isRejected) { + this.setData({ + applyStatus: 'rejected', + statusTitle: '申请未通过', + statusDesc: data.rejectReason || '很抱歉,您的申请未通过审核,您可以修改信息后重新申请' + }) + } else if (data.canApply) { + // 可以申请(没有申请记录或被拒绝后可重新申请) + this.setData({ applyStatus: 'none' }) + } else { + // 兼容旧格式,使用 status 字段 + const status = data.status + if (status) { + this.setData({ + applyStatus: status, + statusTitle: this.getStatusTitle(status), + statusDesc: this.getStatusDesc(status, data.rejectReason || data.reject_reason) + }) + } else { + this.setData({ applyStatus: 'none' }) + } + } + } else { + console.log('没有申请记录,显示申请表单') + this.setData({ applyStatus: 'none' }) + } + } catch (err) { + console.error('获取申请状态失败:', err) + this.setData({ applyStatus: 'none' }) + } finally { + wx.hideLoading() + } + }, + + // 获取状态标题 + getStatusTitle(status) { + const titles = { + 'pending': '正在审核中', + 'reviewing': '正在审核中', + 'approved': '您的资料已审核通过', + 'rejected': '申请未通过' + } + return titles[status] || '正在审核中' + }, + + // 获取状态描述 + getStatusDesc(status, rejectReason) { + const descs = { + 'pending': '您已提交陪聊师申请,正在审核中,请耐心等待', + 'reviewing': '您已提交陪聊师申请,正在审核中,请耐心等待', + 'approved': '恭喜您成为陪聊师!现在可以进入工作台开始接单了', + 'rejected': rejectReason || '很抱歉,您的申请未通过审核,您可以修改信息后重新申请' + } + return descs[status] || '您的申请正在处理中' + }, + + // 重新申请 + reapply() { + this.setData({ showForm: true, applyStatus: 'none' }) + }, + + // 输入变化 + onInputChange(e) { + const field = e.currentTarget.dataset.field + const value = e.detail.value + this.setData({ + [`formData.${field}`]: value + }) + this.checkCanSubmit() + }, + + // 切换协议同意状态 + toggleAgreement() { + this.setData({ + agreed: !this.data.agreed + }) + this.checkCanSubmit() + }, + + // 查看协议 + viewAgreement() { + wx.navigateTo({ + url: '/pages/agreement/agreement?code=cooperation_service' + }) + }, + + // 检查是否可以提交 + checkCanSubmit() { + const { formData, agreed } = this.data + + const canSubmit = + formData.realName && + formData.phone && + formData.phone.length === 11 && + agreed + + this.setData({ canSubmit }) + }, + + // 提交申请 + async submitApply() { + if (!this.data.canSubmit) return + + const { formData } = this.data + + // 验证手机号 + if (!/^1[3-9]\d{9}$/.test(formData.phone)) { + wx.showToast({ title: '请输入正确的手机号', icon: 'none' }) + return + } + + wx.showLoading({ title: '提交中...' }) + try { + const res = await api.companion.apply({ + realName: formData.realName, + city: formData.city, + phone: formData.phone, + remarks: formData.remarks + }) + + if (res.success || res.code === 0) { + wx.showToast({ title: '申请已提交', icon: 'success' }) + this.setData({ + applyStatus: 'pending', + statusTitle: '申请审核中', + statusDesc: '您的申请正在审核中,请耐心等待,我们会尽快处理', + showForm: false + }) + } else { + wx.showToast({ title: res.message || '提交失败', icon: 'none' }) + } + } catch (err) { + console.error('提交申请失败:', err) + wx.showToast({ title: err.message || '提交失败', icon: 'none' }) + } finally { + wx.hideLoading() + } + } +}) diff --git a/pages/companion-apply/companion-apply.json b/pages/companion-apply/companion-apply.json new file mode 100644 index 0000000..8e57e35 --- /dev/null +++ b/pages/companion-apply/companion-apply.json @@ -0,0 +1,5 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "申请成为陪聊师", + "navigationStyle": "custom" +} diff --git a/pages/companion-apply/companion-apply.wxml b/pages/companion-apply/companion-apply.wxml new file mode 100644 index 0000000..2eb0a2b --- /dev/null +++ b/pages/companion-apply/companion-apply.wxml @@ -0,0 +1,115 @@ + + + + + + + + 返回 + + 申请成为陪聊师 + + + + + + + + + + + + + + + + {{statusTitle}} + {{statusDesc}} + + + + + + + + + + + 基本信息 + + + + + 姓名 + * + + + + + + + + + 所在城市 + (选填) + + + + + + + + + 手机号 + * + + + + + + + + + + + 备注信息 + + + + + + {{formData.remarks.length || 0}}/500 + + + + + + + + + + 我已阅读并同意 + 《倾听陪聊师服务协议》 + + + + + + + + + + + + + + 我是陪聊师,进入工作台 + + + diff --git a/pages/companion-apply/companion-apply.wxss b/pages/companion-apply/companion-apply.wxss new file mode 100644 index 0000000..0aa76c0 --- /dev/null +++ b/pages/companion-apply/companion-apply.wxss @@ -0,0 +1,453 @@ +/* 陪聊师申请页面样式 - 根据 Figma 设计重构 */ +.page-container { + min-height: 100vh; + background: #FCE7F3; +} + +/* 顶部导航栏 */ +.nav-header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + background: #FCE7F3; + box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1); +} + +.nav-content { + height: 96rpx; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 32rpx; +} + +.nav-back { + display: flex; + align-items: center; + gap: 4rpx; + padding: 16rpx; + margin-left: -16rpx; +} + +.back-icon { + width: 56rpx; + height: 56rpx; +} + +.back-text { + font-size: 34rpx; + font-weight: 700; + color: #101828; +} + +.nav-title { + font-size: 40rpx; + font-weight: 700; + color: #101828; +} + +.nav-right { + width: 160rpx; + display: flex; + justify-content: flex-end; + align-items: center; + gap: 24rpx; +} + +.more-icon { + width: 48rpx; + height: 48rpx; +} + +/* 内容滚动区域 */ +.content-scroll { + min-height: 100vh; + padding-bottom: 200rpx; +} + +/* 状态卡片 */ +.status-card { + margin: 32rpx; + background: #fff; + border-radius: 60rpx; + padding: 80rpx 48rpx; + text-align: center; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.status-icon { + width: 200rpx; + height: 200rpx; + margin: 0 auto 40rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.status-icon.pending, +.status-icon.reviewing { + background: #FFF3E0; +} + +.status-icon.approved { + background: #E8F5E9; +} + +.status-icon.rejected { + background: #FFEBEE; +} + +.status-icon image { + width: 100rpx; + height: 100rpx; +} + +.status-title { + display: block; + font-size: 44rpx; + font-weight: 700; + color: #1F2937; + margin-bottom: 20rpx; +} + +.status-desc { + display: block; + font-size: 30rpx; + color: #9CA3AF; + margin-bottom: 48rpx; + line-height: 1.5; +} + +.btn-primary { + background: linear-gradient(135deg, #b06ab3 0%, #d4a5d6 100%); + color: #fff; + font-size: 34rpx; + font-weight: 700; + padding: 28rpx 80rpx; + border-radius: 60rpx; + border: none; +} + +.btn-secondary { + background: #F3F4F6; + color: #4B5563; + font-size: 34rpx; + font-weight: 700; + padding: 28rpx 80rpx; + border-radius: 60rpx; + border: none; +} + +/* 申请表单 */ +.apply-form { + margin: 16rpx 0 0; + background: #fff; + border-radius: 60rpx 60rpx 0 0; + padding: 64rpx 48rpx; + min-height: calc(100vh - 200rpx); +} + +/* 表单头部 */ +.form-header { + text-align: center; + margin-bottom: 64rpx; +} + +.form-title { + display: block; + font-size: 44rpx; + font-weight: 700; + color: #1F2937; + margin-bottom: 16rpx; +} + +.form-subtitle { + display: block; + font-size: 28rpx; + color: #9CA3AF; +} + +/* 表单区块 */ +.form-section { + margin-bottom: 64rpx; +} + +.section-header { + display: flex; + align-items: center; + margin-bottom: 32rpx; +} + +.section-title { + font-size: 34rpx; + font-weight: 700; + color: #1F2937; +} + +.required { + color: #F87171; + margin-left: 4rpx; + font-size: 34rpx; +} + +/* 头像上传 */ +.avatar-upload-area { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 24rpx; +} + +.avatar-circle { + width: 224rpx; + height: 224rpx; + border-radius: 50%; + background: #F3F4F6; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.avatar-image { + width: 100%; + height: 100%; +} + +.upload-placeholder { + display: flex; + flex-direction: column; + align-items: center; + gap: 8rpx; +} + +.camera-icon { + width: 64rpx; + height: 64rpx; + opacity: 0.6; +} + +.upload-text { + font-size: 24rpx; + color: #9CA3AF; +} + +.form-tip { + display: block; + text-align: center; + font-size: 24rpx; + color: #9CA3AF; +} + +/* 表单项 */ +.form-item { + margin-bottom: 40rpx; +} + +.item-label-row { + display: flex; + align-items: center; + margin-bottom: 16rpx; +} + +.item-label { + font-size: 30rpx; + color: #374151; +} + +.input-wrapper { + background: #F9FAFB; + border-radius: 28rpx; + padding: 0 32rpx; + height: 100rpx; + display: flex; + align-items: center; +} + +.item-input { + width: 100%; + height: 100%; + font-size: 30rpx; + color: #1F2937; +} + +.placeholder { + color: #9CA3AF; +} + +/* 性别选择 */ +.gender-options { + display: flex; + gap: 32rpx; +} + +.gender-btn { + flex: 1; + height: 100rpx; + background: #F9FAFB; + border-radius: 28rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 30rpx; + color: #4B5563; + transition: all 0.3s; +} + +.gender-btn.active { + background: linear-gradient(135deg, #b06ab3 0%, #d4a5d6 100%); + color: #fff; +} + +/* 服务类型 */ +.service-types { + display: flex; + gap: 24rpx; +} + +.service-btn { + flex: 1; + height: 88rpx; + background: #F9FAFB; + border-radius: 28rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 26rpx; + color: #4B5563; + transition: all 0.3s; +} + +.service-btn.active { + background: #FCE7F3; + color: #b06ab3; + border: 2rpx solid #b06ab3; +} + +/* 个人介绍 */ +.textarea-wrapper { + background: #F9FAFB; + border-radius: 28rpx; + padding: 32rpx; +} + +.intro-textarea { + width: 100%; + height: 320rpx; + font-size: 30rpx; + color: #1F2937; + line-height: 1.5; +} + +.textarea-footer { + display: flex; + justify-content: flex-end; + margin-top: 16rpx; +} + +.char-count { + font-size: 24rpx; + color: #9CA3AF; +} + +/* 协议 */ +.agreement-row { + display: flex; + align-items: center; + gap: 16rpx; + margin: 48rpx 0; +} + +.checkbox { + width: 40rpx; + height: 40rpx; + border: 2rpx solid #D1D5DC; + border-radius: 8rpx; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.checkbox.checked { + background: #b06ab3; + border-color: #b06ab3; +} + +.check-icon { + width: 24rpx; + height: 24rpx; +} + +.agreement-text { + display: flex; + flex-wrap: wrap; + align-items: center; +} + +.normal-text { + font-size: 26rpx; + color: #4B5563; +} + +.link-text { + font-size: 26rpx; + color: #BE185D; +} + +/* 提交按钮 */ +.submit-btn { + width: 100%; + height: 108rpx; + background: linear-gradient(135deg, #b06ab3 0%, #d4a5d6 100%); + color: #fff; + font-size: 36rpx; + font-weight: 700; + border-radius: 60rpx; + border: none; + display: flex; + align-items: center; + justify-content: center; +} + +.submit-btn.disabled { + background: #F3F4F6; + color: #D1D5DB; +} + +/* 底部占位 */ +.bottom-placeholder { + height: 120rpx; +} + +/* 浮动按钮 */ +.float-btn { + position: fixed; + bottom: 140rpx; + left: 50%; + transform: translateX(-50%); + background: rgba(255, 255, 255, 0.9); + border: 2rpx solid rgba(255, 107, 107, 0.2); + border-radius: 100rpx; + padding: 24rpx 50rpx; + display: flex; + align-items: center; + gap: 16rpx; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1), 0 20rpx 30rpx rgba(0, 0, 0, 0.1); + z-index: 100; +} + +.float-btn-text { + font-size: 28rpx; + font-weight: 700; + color: #FF6B6B; +} + +.float-btn-arrow { + width: 32rpx; + height: 32rpx; +} diff --git a/pages/companion-chat/companion-chat.js b/pages/companion-chat/companion-chat.js new file mode 100644 index 0000000..ad3fe07 --- /dev/null +++ b/pages/companion-chat/companion-chat.js @@ -0,0 +1,751 @@ +// pages/companion-chat/companion-chat.js - 陪聊师列表/陪聊聊天页面 +// 对接后端API + +const api = require('../../utils/api') +const { getPageAssets } = require('../../utils/assets') +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + stickyHeaderHeight: 150, + isSticky: false, + stickyThreshold: 0, + loading: false, + showCategoryGrid: true, + categoryImageLoadCount: 0, + categoryImageErrorCount: 0, + // 页面素材 + bannerImage: 'https://ai-c.maimanji.com/images/Header-banner.png', + categoryImages: [ + 'https://ai-c.maimanji.com/images/pb01.png', + 'https://ai-c.maimanji.com/images/pb02.png', + 'https://ai-c.maimanji.com/images/pb03.png', + 'https://ai-c.maimanji.com/images/pb04.png' + ], + consultButtonImage: '/images/btn-text-consult.png', + giftIcon: '/images/icon-gift.png', + locationIcon: '/images/icon-location.png', + // 页面模式: list(陪聊师列表) / chat(聊天) + mode: 'list', + // 列表模式数据 + searchKeyword: '', + counselorList: [], + filters: { + status: '', + gender: '', + sortBy: 'rating', + specialty: '' + }, + // 筛选条显示文本 + filterLabels: { + sort: '排序', + gender: '性别', + specialty: '类型', + filter: '筛选' + }, + page: 1, + hasMore: true, + // 聊天模式数据 + orderId: '', + companionId: '', + companionName: '', + messages: [], + inputText: '', + serviceEndTime: null, + remainingTime: '', + // 未读消息数 + totalUnread: 0, + // 电话倾诉指南弹窗 + showGuidePopup: false, + guideData: { + title: '电话倾诉指南', + subtitle: '让沟通更有效', + steps: [ + { number: 1, title: '选择合适的倾诉师', description: '根据您的需求,浏览倾诉师的擅长方向、服务经验和用户评价,选择最适合您的专业倾诉师。' }, + { number: 2, title: '预约通话时间', description: '选择您方便的时间段进行预约,确保有充足的时间进行深入交流,建议每次通话30-60分钟。' }, + { number: 3, title: '准备倾诉内容', description: '提前整理您想要倾诉的问题或困惑,这样能让沟通更有针对性和效果。' }, + { number: 4, title: '保持真诚开放', description: '在安全私密的环境中,真诚地表达您的感受和想法。倾诉师会为您保密,请放心倾诉。' } + ], + tips: { + title: '温馨提示', + content: '首次通话建议先试听,了解倾诉师的沟通风格。如遇紧急情况,请及时拨打心理危机热线或就医。' + } + }, + auditStatus: 0 + }, + + async onLoad(options) { + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + // 计算固定头部高度(搜索框 + 筛选条) + const stickyHeaderHeight = statusBarHeight + 90 + + // 计算吸顶触发阈值:banner高度(400rpx) + 红包条(约80rpx) + 分类按钮(约230rpx) + 标题行(约80rpx) + // 转换为px:rpx * 屏幕宽度 / 750 + const screenWidth = systemInfo.screenWidth + const stickyThreshold = (400 + 80 + 230 + 80) * screenWidth / 750 + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight, + stickyHeaderHeight, + stickyThreshold + }) + + // 加载页面素材 + await this.loadPageAssets() + + // 判断页面模式 + if (options.orderId) { + // 聊天模式 + this.setData({ + mode: 'chat', + orderId: options.orderId, + companionId: options.companionId, + companionName: decodeURIComponent(options.name || '') + }) + this.loadChatHistory() + } else { + // 列表模式 + this.loadCounselorList() + } + }, + + onShow() { + wx.hideTabBar({ animation: false }) + const app = getApp() + this.setData({ + auditStatus: app.globalData.auditStatus + }) + this.loadUnreadCount() + }, + + onUnload() { + // 清除计时器 + if (this.timer) { + clearInterval(this.timer) + } + }, + + /** + * 页面滚动监听 - 控制吸顶效果 + */ + onPageScroll(e) { + const scrollTop = e.detail.scrollTop + const { stickyThreshold, isSticky } = this.data + + // 添加一定的缓冲区,避免在临界点频繁切换 + const buffer = 10 // 10px缓冲区 + + // 当滚动超过阈值+缓冲时显示吸顶,低于阈值-缓冲时隐藏 + let shouldSticky = isSticky + if (scrollTop > stickyThreshold + buffer) { + shouldSticky = true + } else if (scrollTop < stickyThreshold - buffer) { + shouldSticky = false + } + + if (shouldSticky !== isSticky) { + this.setData({ isSticky: shouldSticky }) + } + }, + + /** + * 加载页面素材配置 + */ + async loadPageAssets() { + try { + const assets = await getPageAssets(); + + if (assets) { + this.setData({ + bannerImage: assets.banners.companion_banner, + categoryImages: [ + assets.entries.entry_1, + assets.entries.entry_2, + assets.entries.entry_3, + assets.entries.entry_4 + ], + consultButtonImage: assets.icons.consult_button, + giftIcon: assets.icons.gift, + locationIcon: assets.icons.location + }); + } + } catch (error) { + console.error('加载页面素材失败:', error); + // 使用默认值,已在 data 中定义 + } + }, + + /** + * 加载未读消息数 + */ + async loadUnreadCount() { + if (!app.globalData.isLoggedIn) { + this.setData({ totalUnread: 0 }) + return + } + + try { + const res = await api.chat.getConversations() + if (res.success && res.data) { + const totalUnread = res.data.reduce((sum, conv) => sum + (conv.unread_count || 0), 0) + this.setData({ totalUnread }) + } else { + this.setData({ totalUnread: 0 }) + } + } catch (err) { + console.log('获取未读消息数失败', err) + this.setData({ totalUnread: 0 }) + } + }, + + // ==================== 列表模式 ==================== + + /** + * 加载陪聊师列表 + */ + async loadCounselorList() { + this.setData({ loading: true, page: 1 }) + + try { + const params = { + page: 1, + pageSize: 20, + ...this.data.filters + } + + // 过滤掉空值参数 + Object.keys(params).forEach(key => { + if (params[key] === '' || params[key] === undefined || params[key] === null) { + delete params[key] + } + }) + + if (this.data.searchKeyword.trim()) { + params.keyword = this.data.searchKeyword.trim() + } + + console.log('请求陪聊师列表,参数:', params) + + const res = await api.companion.getList(params) + + console.log('陪聊师列表响应:', res) + + // 兼容两种返回格式 + let list = [] + if (res.success && res.data) { + // 格式1: { success: true, data: { list: [...] } } + // 格式2: { success: true, data: [...] } + list = Array.isArray(res.data) ? res.data : (res.data.list || []) + } + + if (list.length > 0) { + const transformedList = list.map(c => this.transformCounselor(c)) + + this.setData({ + counselorList: transformedList, + hasMore: list.length >= 20, + loading: false + }) + console.log('更新列表成功,数量:', transformedList.length) + } else { + // API没有数据时使用模拟数据 + console.log('API返回空数据,使用模拟数据') + this.setData({ + counselorList: this.getMockCounselorList(), + loading: false + }) + } + } catch (err) { + console.error('加载陪聊师列表失败', err) + // 加载失败时使用模拟数据 + this.setData({ + counselorList: this.getMockCounselorList(), + loading: false + }) + } + }, + + /** + * 获取模拟陪聊师数据 + */ + getMockCounselorList() { + return [ + { + id: 'c001', + name: '林心怡', + avatar: '', + avatarColor: '#e8b4d8', + avatarColorEnd: '#c984cd', + type: '文字/语音', + age: '28岁', + education: '心理学硕士', + training: '国家二级心理咨询师', + certification: '情感咨询专家认证', + quote: '每一次倾诉,都是心灵的释放', + tags: ['情感倾诉', '婚姻家庭', '亲密关系'], + serviceCount: 1286, + repeatCount: 423, + rating: 4.96, + location: '北京', + online: true + }, + { + id: 'c002', + name: '张明辉', + avatar: '', + avatarColor: '#a8d8ea', + avatarColorEnd: '#6bb3d9', + type: '文字/语音', + age: '35岁', + education: '应用心理学博士', + training: '高级心理咨询师', + certification: '职场心理专家', + quote: '用专业的态度,温暖每一颗心', + tags: ['职场压力', '人际关系', '情绪管理'], + serviceCount: 2156, + repeatCount: 687, + rating: 4.92, + location: '上海', + online: true + }, + { + id: 'c003', + name: '王雨萱', + avatar: '', + avatarColor: '#f8c8dc', + avatarColorEnd: '#e89bb8', + type: '文字/语音', + age: '26岁', + education: '心理学学士', + training: '情感咨询师', + certification: '青年心理辅导员', + quote: '倾听你的故事,陪伴你的成长', + tags: ['恋爱指导', '分手挽回', '单身脱单'], + serviceCount: 856, + repeatCount: 298, + rating: 4.89, + location: '深圳', + online: false + }, + { + id: 'c004', + name: '李思远', + avatar: '', + avatarColor: '#b8d4e3', + avatarColorEnd: '#8ab4cf', + type: '文字/语音', + age: '42岁', + education: '临床心理学硕士', + training: '资深心理治疗师', + certification: '家庭治疗师认证', + quote: '专业倾听,用心陪伴每一刻', + tags: ['婚姻危机', '家庭矛盾', '亲子教育'], + serviceCount: 3421, + repeatCount: 1156, + rating: 4.98, + location: '广州', + online: true + }, + { + id: 'c005', + name: '陈晓琳', + avatar: '', + avatarColor: '#d4b8e8', + avatarColorEnd: '#b088d4', + type: '文字/语音', + age: '31岁', + education: '发展心理学硕士', + training: '心理咨询师', + certification: '情绪管理专家', + quote: '让每一次对话都充满温暖', + tags: ['焦虑抑郁', '情绪调节', '自我成长'], + serviceCount: 1567, + repeatCount: 512, + rating: 4.94, + location: '杭州', + online: true + }, + { + id: 'c006', + name: '赵文博', + avatar: '', + avatarColor: '#a8e6cf', + avatarColorEnd: '#7bc9a6', + type: '文字/语音', + age: '38岁', + education: '社会心理学博士', + training: '高级心理顾问', + certification: '企业EAP咨询师', + quote: '理性分析,感性陪伴', + tags: ['职业规划', '压力管理', '领导力'], + serviceCount: 1892, + repeatCount: 634, + rating: 4.91, + location: '成都', + online: false + } + ] + }, + + /** + * 转换陪聊师数据格式 + */ + transformCounselor(data) { + const config = require('../../config/index') + + // 处理地址,只显示城市名 + let location = data.location || data.city || '' + if (location) { + // 如果地址包含多个部分(如"北京 北京市 朝阳区"),只取第一个城市名 + // 或者如果是"XX市"格式,去掉"市"字 + const parts = location.split(/[\s,,]+/).filter(p => p.trim()) + if (parts.length > 0) { + location = parts[0].replace(/[省市区县]$/, '') || parts[0] + } + } + + // 处理在线状态 - 后端返回 onlineStatus 或 status + const onlineStatus = data.onlineStatus || data.online_status || data.status || 'offline' + const isOnline = onlineStatus === 'online' + const isBusy = onlineStatus === 'busy' + + // 处理头像URL - 如果是相对路径,拼接完整域名 + let avatar = data.avatar || '' + if (avatar && avatar.startsWith('/')) { + // 从 API_BASE_URL 提取域名(去掉 /api 后缀) + const baseUrl = config.API_BASE_URL.replace(/\/api$/, '') + avatar = baseUrl + avatar + } + + return { + id: data.id, + name: data.name || data.displayName || data.nickname, + avatar: avatar, + type: data.service_type || '文字/语音', + age: data.age_group || data.age || '', + education: data.education || '', + training: data.training || data.certification || '', + certification: data.certification || '', + quote: data.quote || data.bio || data.introduction || '', + tags: data.tags || data.specialties || [], + serviceCount: data.serviceCount || data.service_count || data.totalOrders || 0, + repeatCount: data.repeatCount || data.repeat_count || 0, + rating: data.rating || 5.0, + location: location, + online: isOnline, + onlineStatus: onlineStatus, + statusText: data.statusText || (isOnline ? '在线' : isBusy ? '忙碌中' : '离线'), + // 等级和价格信息 + levelCode: data.levelCode || 'junior', + levelName: data.levelName || '初级', + textPrice: data.textPrice || data.pricePerMinute || 0.5, + voicePrice: data.voicePrice || 1 + } + }, + + onSearchInput(e) { + this.setData({ searchKeyword: e.detail.value }) + }, + + /** + * 搜索 + */ + onSearch() { + this.loadCounselorList() + }, + + onCategoryTap(e) { + const type = e.currentTarget.dataset.type + wx.showToast({ title: type, icon: 'none' }) + }, + + /** + * 分类图片加载成功 + */ + onCategoryImageLoad() { + const count = this.data.categoryImageLoadCount + 1 + this.setData({ categoryImageLoadCount: count }) + }, + + /** + * 分类图片加载失败 - 如果全部失败则隐藏区域 + */ + onCategoryImageError() { + const errorCount = this.data.categoryImageErrorCount + 1 + this.setData({ categoryImageErrorCount: errorCount }) + + // 如果4张图片都加载失败,隐藏整个分类区域 + if (errorCount >= 4) { + this.setData({ showCategoryGrid: false }) + } + }, + + onFilterTap(e) { + const filter = e.currentTarget.dataset.filter + console.log('筛选点击:', filter) + + const options = this.getFilterOptions(filter) + console.log('筛选选项:', options) + + if (!options || options.length === 0) { + wx.showToast({ title: '暂无筛选选项', icon: 'none' }) + return + } + + wx.showActionSheet({ + itemList: options, + success: (res) => { + console.log('选择了:', res.tapIndex, options[res.tapIndex]) + const value = this.getFilterValue(filter, res.tapIndex) + const label = options[res.tapIndex] + + // 更新筛选值 - 映射筛选类型到正确的参数名 + // sort -> sortBy, filter -> status, 其他保持不变 + const filterKeyMap = { + sort: 'sortBy', + filter: 'status', // 筛选选项对应后端的 status 参数 + gender: 'gender', + specialty: 'specialty' + } + const filterKey = filterKeyMap[filter] || filter + + // 更新筛选标签显示 + const defaultLabels = { + sort: '排序', + gender: '性别', + specialty: '类型', + filter: '筛选' + } + const displayLabel = res.tapIndex === 0 ? defaultLabels[filter] : label + + console.log('更新筛选参数:', filterKey, '=', value) + + this.setData({ + [`filters.${filterKey}`]: value, + [`filterLabels.${filter}`]: displayLabel + }) + + this.loadCounselorList() + }, + fail: (err) => { + console.log('取消选择或失败:', err) + } + }) + }, + + getFilterOptions(filter) { + const options = { + sort: ['综合排序', '好评优先', '服务人次', '价格从低到高'], + gender: ['不限', '男', '女'], + specialty: ['不限', '情感倾诉', '婚姻家庭', '职场压力', '亲子关系'], + filter: ['在线优先', '有空闲', '全部'] + } + return options[filter] || [] + }, + + getFilterValue(filter, index) { + const values = { + sort: ['', 'rating', 'service_count', 'price'], + gender: ['', 'male', 'female'], + specialty: ['', 'emotion', 'marriage', 'work', 'family'], + filter: ['online', 'available', ''] + } + return (values[filter] || [])[index] || '' + }, + + async onGuideClick() { + // 显示弹窗 + this.setData({ showGuidePopup: true }) + + // 从后台获取协议内容 + try { + const res = await api.agreement.get('phone-guide') + if (res.success && res.data && res.data.content) { + // 解析JSON内容 + let content = res.data.content + if (typeof content === 'string') { + try { + content = JSON.parse(content) + } catch (e) { + console.log('协议内容不是JSON格式') + return + } + } + + // 更新弹窗数据 + this.setData({ + 'guideData.subtitle': content.subtitle || '让沟通更有效', + 'guideData.steps': content.steps || this.data.guideData.steps, + 'guideData.tips': content.tips || this.data.guideData.tips + }) + } + } catch (err) { + console.log('获取协议内容失败,使用默认内容', err) + } + }, + + /** + * 关闭指南弹窗 + */ + onCloseGuidePopup() { + this.setData({ showGuidePopup: false }) + }, + + /** + * 阻止弹窗内容区域的点击事件冒泡 + */ + preventClose() { + // 空函数,阻止事件冒泡 + }, + + onCounselorTap(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/counselor-detail/counselor-detail?id=${id}` + }) + }, + + /** + * 头像加载失败时,清空avatar字段让其显示占位符 + */ + onAvatarError(e) { + const index = e.currentTarget.dataset.index + if (index !== undefined) { + this.setData({ + [`counselorList[${index}].avatar`]: '' + }) + } + }, + + onTrialListen(e) { + const id = e.currentTarget.dataset.id + wx.showToast({ title: '正在播放试听...', icon: 'none' }) + }, + + onConsult(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/counselor-detail/counselor-detail?id=${id}` + }) + }, + + // ==================== 聊天模式 ==================== + + /** + * 加载聊天历史 + */ + async loadChatHistory() { + this.setData({ loading: true }) + + try { + const res = await api.chat.getChatHistory(this.data.orderId, { + page: 1, + limit: 50 + }) + + if (res.success && res.data) { + const messages = (res.data || []).map(msg => ({ + id: msg.id, + content: msg.content, + isMe: msg.sender_type === 'user', + time: this.formatTime(msg.created_at), + type: msg.message_type || 'text' + })) + + this.setData({ messages, loading: false }) + this.scrollToBottom() + } else { + this.setData({ loading: false }) + } + } catch (err) { + console.error('加载聊天历史失败', err) + this.setData({ loading: false }) + } + }, + + /** + * 发送消息 + */ + async sendMessage() { + const { inputText, orderId } = this.data + + if (!inputText.trim()) return + + const messageText = inputText.trim() + + // 添加到消息列表 + const newMessage = { + id: Date.now(), + content: messageText, + isMe: true, + time: this.formatTime(new Date()), + type: 'text' + } + + this.setData({ + messages: [...this.data.messages, newMessage], + inputText: '' + }) + + this.scrollToBottom() + + try { + await api.companion.sendMessage({ + order_id: orderId, + message: messageText + }) + } catch (err) { + console.error('发送消息失败', err) + wx.showToast({ title: '发送失败', icon: 'none' }) + } + }, + + onInput(e) { + this.setData({ inputText: e.detail.value }) + }, + + /** + * 格式化时间 + */ + formatTime(dateStr) { + const date = new Date(dateStr) + const hour = String(date.getHours()).padStart(2, '0') + const minute = String(date.getMinutes()).padStart(2, '0') + return `${hour}:${minute}` + }, + + scrollToBottom() { + // 滚动到底部 + }, + + onBack() { + wx.navigateBack() + }, + + // Tab bar navigation - 需要登录的页面检查登录状态 + switchTab(e) { + const path = e.currentTarget.dataset.path + const app = getApp() + + // 消息和我的页面需要登录 + if (path === '/pages/chat/chat' || path === '/pages/profile/profile') { + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ + url: '/pages/login/login?redirect=' + encodeURIComponent(path) + }) + return + } + } + wx.switchTab({ url: path }) + } +}) diff --git a/pages/companion-chat/companion-chat.json b/pages/companion-chat/companion-chat.json new file mode 100644 index 0000000..e90e996 --- /dev/null +++ b/pages/companion-chat/companion-chat.json @@ -0,0 +1,4 @@ +{ + "usingComponents": {}, + "navigationStyle": "custom" +} diff --git a/pages/companion-chat/companion-chat.wxml b/pages/companion-chat/companion-chat.wxml new file mode 100644 index 0000000..6c00f39 --- /dev/null +++ b/pages/companion-chat/companion-chat.wxml @@ -0,0 +1,236 @@ + + + + + + + + + + + + + + {{filterLabels.sort}} + + + + {{filterLabels.gender}} + + + + {{filterLabels.specialty}} + + + + {{filterLabels.filter}} + + + + + + + + + + + + + + + + 您有1个红包即将过期,请及时使用哦 + + + + + + + + + + + + + + + + + + + + + + + 倾听陪伴 + + 电话倾诉指南 + + + + + + + + + + + + + + {{filterLabels.sort}} + + + + {{filterLabels.gender}} + + + + {{filterLabels.specialty}} + + + + {{filterLabels.filter}} + + + + + + + + + + + + + + + + + {{item.name[0]}} + + + + {{item.statusText}} + + + 试听 + + + + + + + {{item.name}} + {{item.type}} + + {{item.age}} {{item.education}} {{item.training}} + {{item.certification}} + "{{item.quote}}" + + + {{tag}} + + + + + {{item.serviceCount}} + 服务人次 + + + {{item.rating}} + 评分 + + + + + + + + + + {{item.location}} + + + + + + + + + + + + + + + 陪伴 + + + + 文娱 + + + + 服务 + + + + + + 消息 + + + + 我的 + + + + + + + + + + + + + + {{guideData.title}} + {{guideData.subtitle}} + + + + + + + + + + + {{item.number}} + + {{item.title}} + {{item.description}} + + + + + + + + ! + + + {{guideData.tips.title}} + {{guideData.tips.content}} + + + + + + + + + + diff --git a/pages/companion-chat/companion-chat.wxss b/pages/companion-chat/companion-chat.wxss new file mode 100644 index 0000000..2629a46 --- /dev/null +++ b/pages/companion-chat/companion-chat.wxss @@ -0,0 +1,817 @@ +/* 陪聊页面样式 */ +page { + background: #fff; +} + +.page-container { + min-height: 100vh; + background: #fff; + position: relative; +} + +/* 吸顶搜索和筛选区域 */ +.sticky-header { + position: fixed; + top: 0; + left: 0; + right: 0; + width: 100%; + z-index: 998; + background-color: #ffffff; + padding: 16rpx 32rpx 24rpx; + padding-top: calc(16rpx + var(--status-bar-height, 44px)); + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); + opacity: 0; + pointer-events: none; + visibility: hidden; +} + +.sticky-header::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ffffff; + z-index: -1; +} + +.sticky-header.show { + opacity: 1; + pointer-events: auto; + visibility: visible; +} + +/* 吸顶内的搜索框 */ +.sticky-header .search-box { + background: #f3f4f6; + position: relative; + z-index: 1; +} + +/* 吸顶内的筛选条 */ +.sticky-header .filter-bar { + background: transparent; + position: relative; + z-index: 1; +} + +/* 跟随滚动的 Header 背景 */ +.header-scroll { + height: 400rpx; + overflow: hidden; + /* 备用渐变背景,当图片加载失败时显示 */ + background: linear-gradient(135deg, #e8d5f0 0%, #f5e6d3 50%, #fce4ec 100%); +} + +.header-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* 隐藏滚动条 */ +.content-scroll::-webkit-scrollbar { + display: none; + width: 0; + height: 0; +} + +.content-scroll { + -ms-overflow-style: none; + scrollbar-width: none; + height: 100vh; + box-sizing: border-box; + position: relative; + z-index: 1; +} + +/* 红包提示条 */ +.notice-bar { + padding: 24rpx 32rpx; + background: linear-gradient(to right, #fffbeb, #fff7ed); + border-bottom: 2rpx solid rgba(254, 243, 198, 0.5); + display: flex; + align-items: center; + gap: 16rpx; +} + +.notice-icon { + width: 40rpx; + height: 40rpx; +} + +.notice-text { + font-size: 32rpx; + color: #973c00; +} + +/* 分类按钮 */ +.category-grid { + display: flex; + gap: 16rpx; + padding: 24rpx 32rpx; +} + +.category-btn { + flex: 1; + height: 180rpx; + border-radius: 24rpx; + overflow: hidden; + position: relative; +} + +.category-image { + width: 100%; + height: 100%; + border-radius: 24rpx; +} + +/* 倾诉师列表区域 */ +.counselor-section { + padding: 24rpx 32rpx; + background: #fff; + border-radius: 48rpx 48rpx 0 0; + margin-top: -24rpx; +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 32rpx; +} + +.section-title { + font-size: 40rpx; + font-weight: 700; + color: #1e2939; +} + +.guide-btn { + display: flex; + align-items: center; + gap: 4rpx; +} + +.guide-text { + font-size: 24rpx; + color: #101828; +} + +.guide-arrow { + width: 24rpx; + height: 24rpx; +} + +/* 搜索框 */ +.search-box { + display: flex; + align-items: center; + background: #f3f4f6; + border-radius: 100rpx; + padding: 20rpx 32rpx; + margin-bottom: 16rpx; +} + +.search-box.hidden { + display: none; +} + +.search-icon { + width: 40rpx; + height: 40rpx; + margin-right: 16rpx; +} + +.search-input { + flex: 1; + font-size: 28rpx; + color: #101828; +} + +.search-input::placeholder { + color: rgba(16, 24, 40, 0.5); +} + +/* 筛选条 */ +.filter-bar { + display: flex; + justify-content: space-between; + padding-bottom: 0; + border-bottom: none; + margin-bottom: 0; +} + +.filter-bar-inline { + padding-bottom: 24rpx; + border-bottom: 2rpx solid #f3f4f6; + margin-bottom: 24rpx; +} + +.filter-bar.hidden { + display: none; +} + +.filter-item { + display: flex; + align-items: center; + gap: 4rpx; + padding: 16rpx 8rpx; + min-height: 60rpx; +} + +.filter-item.active .filter-text { + color: #9333ea; + font-weight: 500; +} + +.filter-text { + font-size: 32rpx; + color: #364153; +} + +.filter-arrow { + width: 32rpx; + height: 32rpx; + opacity: 0.6; +} + +/* 吸顶占位 */ +.sticky-placeholder { + height: 0; +} + +.sticky-placeholder.show { + height: 180rpx; +} + +/* 倾诉师列表 */ +.counselor-list { + display: flex; + flex-direction: column; +} + +.counselor-card { + display: flex; + padding: 16rpx 0; + border-bottom: 2rpx solid #f3f4f6; + position: relative; +} + +.counselor-card:last-child { + border-bottom: none; +} + +/* 左侧头像区域 */ +.counselor-avatar-section { + display: flex; + flex-direction: column; + align-items: center; + width: 120rpx; + margin-right: 16rpx; + flex-shrink: 0; +} + +.avatar-wrap { + width: 120rpx; + height: 120rpx; + border-radius: 50%; + background: #e5e7eb; + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.avatar-placeholder { + width: 100%; + height: 100%; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +/* 真实头像图片样式 */ +.avatar-image { + width: 100%; + height: 100%; + border-radius: 50%; +} + +.avatar-text { + font-size: 40rpx; + font-weight: 700; + color: #fff; +} + +.online-dot { + position: absolute; + bottom: 4rpx; + right: 4rpx; + width: 24rpx; + height: 24rpx; + background: #22c55e; + border-radius: 50%; + border: 4rpx solid #fff; + box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1); +} + +/* 忙碌状态 - 橙色 */ +.online-dot.busy { + background: #f59e0b; +} + +/* 状态文字 */ +.status-text { + font-size: 22rpx; + margin-top: 8rpx; + padding: 4rpx 12rpx; + border-radius: 20rpx; + text-align: center; +} + +.status-text.online { + color: #22c55e; + background: rgba(34, 197, 94, 0.1); +} + +.status-text.busy { + color: #f59e0b; + background: rgba(245, 158, 11, 0.1); +} + +.status-text.offline { + color: #9ca3af; + background: rgba(156, 163, 175, 0.1); +} + +.trial-btn { + display: flex; + align-items: center; + gap: 8rpx; + background: #eff6ff; + border-radius: 100rpx; + padding: 8rpx 16rpx; + margin-top: 16rpx; +} + +.play-icon { + width: 0; + height: 0; + border-left: 10rpx solid #2b7fff; + border-top: 6rpx solid transparent; + border-bottom: 6rpx solid transparent; +} + +.trial-text { + font-size: 28rpx; + color: #155dfc; +} + +.location-row { + display: inline-flex; + align-items: center; + gap: 4rpx; + margin-top: 4rpx; + float: right; +} + +.location-icon { + width: 24rpx; + height: 24rpx; + flex-shrink: 0; +} + +.location-text { + font-size: 26rpx; + color: #6a7282; +} + +/* 中间信息区域 */ +.counselor-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 6rpx; + min-width: 0; + padding-right: 136rpx; +} + +.name-row { + display: flex; + align-items: center; + gap: 16rpx; +} + +.counselor-name { + font-size: 40rpx; + font-weight: 700; + color: #101828; +} + +.counselor-type { + font-size: 28rpx; + color: #6a7282; +} + +.counselor-desc { + font-size: 28rpx; + color: #6a7282; + line-height: 1.5; +} + +.counselor-cert { + font-size: 28rpx; + color: #4a5565; +} + +.counselor-quote { + font-size: 28rpx; + color: #009966; + font-style: normal; + line-height: 1.5; + margin: 8rpx 0; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; + text-overflow: ellipsis; +} + +.tags-row { + display: flex; + gap: 12rpx; + flex-wrap: wrap; +} + +.tag { + background: #f9fafb; + border: 2rpx solid #f3f4f6; + border-radius: 16rpx; + padding: 4rpx 16rpx; +} + +.tag-text { + font-size: 28rpx; + color: #6a7282; +} + +.stats-row { + display: flex; + gap: 16rpx; + margin-top: 8rpx; + flex-wrap: nowrap; + align-items: center; +} + +.stat-item { + display: flex; + align-items: center; + gap: 4rpx; + white-space: nowrap; +} + +.stat-value { + font-size: 28rpx; + font-weight: 700; + color: #4a5565; +} + +.stat-label { + font-size: 28rpx; + color: #99a1af; +} + +/* 右侧咨询按钮和地址 */ +.counselor-action { + position: absolute; + top: 16rpx; + right: 0; + bottom: 16rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; +} + +.consult-btn { + width: 120rpx; + height: 120rpx; +} + +.counselor-action .location-row { + display: flex; + align-items: center; + gap: 4rpx; +} + +.location-icon { + width: 24rpx; + height: 24rpx; + flex-shrink: 0; +} + +.location-text { + font-size: 26rpx; + color: #6a7282; +} + +/* 底部占位 */ +.bottom-placeholder { + height: 240rpx; +} + +/* 自定义底部导航栏 - 完全匹配Figma设计 */ +.custom-tabbar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 194rpx; + background: #fff; + display: flex; + align-items: flex-start; + justify-content: space-around; + padding-top: 24rpx; + z-index: 999; + border-top: 2rpx solid #F3F4F6; +} + +.tabbar-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12rpx; + width: 150rpx; + height: 120rpx; +} + +.tabbar-icon { + width: 68rpx; + height: 68rpx; +} + +.tabbar-text { + font-family: Arial, sans-serif; + font-size: 40rpx; + font-weight: 700; + color: #A58AA5; + line-height: 1; +} + +.tabbar-text.active { + color: #B06AB3; +} + +.message-icon-wrapper { + position: relative; + width: 68rpx; + height: 68rpx; +} + +.message-icon-wrapper .tabbar-icon { + width: 68rpx; + height: 68rpx; +} + +.message-dot { + position: absolute; + top: -8rpx; + right: -8rpx; + width: 24rpx; + height: 24rpx; + background: #FB2C36; + border: 2rpx solid #fff; + border-radius: 50%; +} + + +/* ==================== 电话倾诉指南弹窗样式 ==================== */ + +/* 遮罩层 */ +.guide-popup-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; +} + +/* 弹窗容器 */ +.guide-popup { + width: 680rpx; + background: #fff; + border-radius: 48rpx; + overflow: hidden; + animation: popupIn 0.3s ease-out; + box-shadow: 0 50rpx 100rpx -24rpx rgba(0, 0, 0, 0.25); +} + +@keyframes popupIn { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +/* 紫色渐变头部 */ +.guide-header { + background: linear-gradient(180deg, #b06ab3 0%, #c984cd 100%); + padding: 40rpx 48rpx; + display: flex; + align-items: center; + justify-content: space-between; +} + +.guide-header-left { + display: flex; + align-items: center; + gap: 24rpx; +} + +.guide-phone-icon { + width: 96rpx; + height: 96rpx; + background: rgba(255, 255, 255, 0.2); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.phone-icon-img { + width: 48rpx; + height: 48rpx; +} + +.guide-header-text { + display: flex; + flex-direction: column; + gap: 4rpx; +} + +.guide-title { + font-size: 40rpx; + font-weight: 700; + color: #fff; + line-height: 1.4; +} + +.guide-subtitle { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.8); + line-height: 1.4; +} + +.guide-close-btn { + width: 64rpx; + height: 64rpx; + background: rgba(255, 255, 255, 0.2); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.close-icon-img { + width: 40rpx; + height: 40rpx; +} + +/* 步骤列表 */ +.guide-steps { + padding: 40rpx 48rpx 0; +} + +.guide-step { + display: flex; + align-items: flex-start; + margin-bottom: 40rpx; + gap: 24rpx; +} + +.guide-step:last-child { + margin-bottom: 0; +} + +.step-number { + width: 64rpx; + height: 64rpx; + background: linear-gradient(180deg, #b06ab3 0%, #c984cd 100%); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + font-weight: 700; + color: #fff; + flex-shrink: 0; +} + +.step-content { + flex: 1; + padding-top: 4rpx; +} + +.step-title { + display: block; + font-size: 32rpx; + font-weight: 700; + color: #1e2939; + margin-bottom: 12rpx; + line-height: 1.5; +} + +.step-desc { + display: block; + font-size: 28rpx; + color: #4a5565; + line-height: 1.625; +} + +/* 温馨提示卡片 */ +.guide-tips { + margin: 40rpx 48rpx; + padding: 34rpx; + background: linear-gradient(180deg, #fffbeb 0%, #fff7ed 100%); + border: 2rpx solid #fde68a; + border-radius: 32rpx; + display: flex; + align-items: flex-start; + gap: 28rpx; +} + +.tips-icon-wrap { + width: 40rpx; + height: 40rpx; + background: #fbbf24; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-top: 4rpx; +} + +.tips-icon-text { + font-size: 24rpx; + font-weight: 700; + color: #fff; +} + +.tips-content { + flex: 1; +} + +.tips-title { + display: block; + font-size: 32rpx; + font-weight: 700; + color: #92400e; + margin-bottom: 8rpx; + line-height: 1.5; +} + +.tips-text { + display: block; + font-size: 28rpx; + color: #78350f; + line-height: 1.625; +} + +/* 确认按钮 */ +.guide-btn-wrap { + padding: 0 48rpx 40rpx; +} + +.guide-confirm-btn { + width: 100%; + height: 120rpx; + background: linear-gradient(180deg, #b06ab3 0%, #c984cd 100%); + border-radius: 32rpx; + font-size: 36rpx; + font-weight: 700; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + border: none; + box-shadow: 0 8rpx 12rpx -8rpx rgba(0, 0, 0, 0.1), 0 20rpx 30rpx -6rpx rgba(0, 0, 0, 0.1); +} + +.guide-confirm-btn::after { + border: none; +} diff --git a/pages/companion-orders/companion-orders.js b/pages/companion-orders/companion-orders.js new file mode 100644 index 0000000..ed3cf77 --- /dev/null +++ b/pages/companion-orders/companion-orders.js @@ -0,0 +1,307 @@ +// pages/companion-orders/companion-orders.js +const api = require('../../utils/api') + +Page({ + data: { + currentTab: 'all', + orders: [], + stats: { + totalOrders: 0, + completedOrders: 0, + totalIncome: '0.00' + }, + page: 1, + pageSize: 20, + hasMore: true, + loading: false, + // 评价回复弹窗 + showReplyModal: false, + currentReview: null, + replyContent: '', + submittingReply: false + }, + + onLoad() { + this.loadStats() + this.loadOrders() + }, + + onPullDownRefresh() { + this.setData({ page: 1, hasMore: true }) + Promise.all([this.loadStats(), this.loadOrders()]).finally(() => { + wx.stopPullDownRefresh() + }) + }, + + onReachBottom() { + if (this.data.hasMore && !this.data.loading) { + this.loadMoreOrders() + } + }, + + // 加载统计数据 + async loadStats() { + try { + const res = await api.companion.getOrderStats() + if (res.success) { + this.setData({ + stats: { + totalOrders: res.data.total_orders || 0, + completedOrders: res.data.completed_orders || 0, + totalIncome: (res.data.total_income || 0).toFixed(2) + } + }) + } + } catch (err) { + console.error('加载统计数据失败:', err) + } + }, + + // 加载订单列表 + async loadOrders() { + this.setData({ loading: true }) + try { + const params = { + page: 1, + pageSize: this.data.pageSize + } + if (this.data.currentTab !== 'all') { + params.status = this.data.currentTab + } + + const res = await api.companion.getOrders(params) + if (res.success) { + const orders = (res.data?.list || []).map(order => this.formatOrder(order)) + this.setData({ + orders, + page: 1, + hasMore: orders.length >= this.data.pageSize + }) + } + } catch (err) { + console.error('加载订单失败:', err) + } finally { + this.setData({ loading: false }) + } + }, + + // 加载更多订单 + async loadMoreOrders() { + this.setData({ loading: true }) + try { + const params = { + page: this.data.page + 1, + pageSize: this.data.pageSize + } + if (this.data.currentTab !== 'all') { + params.status = this.data.currentTab + } + + const res = await api.companion.getOrders(params) + if (res.success) { + const newOrders = (res.data?.list || []).map(order => this.formatOrder(order)) + this.setData({ + orders: [...this.data.orders, ...newOrders], + page: this.data.page + 1, + hasMore: newOrders.length >= this.data.pageSize + }) + } + } catch (err) { + console.error('加载更多订单失败:', err) + } finally { + this.setData({ loading: false }) + } + }, + + // 格式化订单数据 + formatOrder(order) { + return { + ...order, + statusText: this.getStatusText(order.status), + serviceTypeText: this.getServiceTypeText(order.service_type), + createTimeText: this.formatTime(order.created_at) + } + }, + + // 获取状态文本 + getStatusText(status) { + const statusMap = { + 'pending': '待服务', + 'in_progress': '进行中', + 'completed': '已完成', + 'cancelled': '已取消' + } + return statusMap[status] || status + }, + + // 获取服务类型文本 + getServiceTypeText(type) { + const typeMap = { + 'chat': '文字聊天', + 'voice': '语音聊天', + 'video': '视频聊天' + } + return typeMap[type] || '聊天服务' + }, + + // 格式化时间 + formatTime(timeStr) { + if (!timeStr) return '' + const date = new Date(timeStr) + const month = date.getMonth() + 1 + const day = date.getDate() + const hour = date.getHours().toString().padStart(2, '0') + const minute = date.getMinutes().toString().padStart(2, '0') + return `${month}/${day} ${hour}:${minute}` + }, + + // 切换标签 + switchTab(e) { + const tab = e.currentTarget.dataset.tab + if (tab === this.data.currentTab) return + + this.setData({ + currentTab: tab, + orders: [], + page: 1, + hasMore: true + }) + this.loadOrders() + }, + + // 开始服务 + async startService(e) { + const orderId = e.currentTarget.dataset.id + + wx.showModal({ + title: '开始服务', + content: '确定要开始服务吗?', + success: async (res) => { + if (res.confirm) { + wx.showLoading({ title: '处理中...' }) + try { + const result = await api.order.startService(orderId) + if (result.success) { + wx.showToast({ title: '服务已开始', icon: 'success' }) + this.loadOrders() + this.loadStats() + } else { + wx.showToast({ title: result.message || '操作失败', icon: 'none' }) + } + } catch (err) { + wx.showToast({ title: '操作失败', icon: 'none' }) + } finally { + wx.hideLoading() + } + } + } + }) + }, + + // 结束服务 + async endService(e) { + const orderId = e.currentTarget.dataset.id + + wx.showModal({ + title: '结束服务', + content: '确定要结束服务吗?', + success: async (res) => { + if (res.confirm) { + wx.showLoading({ title: '处理中...' }) + try { + const result = await api.order.endService(orderId) + if (result.success) { + wx.showToast({ title: '服务已结束', icon: 'success' }) + this.loadOrders() + this.loadStats() + } else { + wx.showToast({ title: result.message || '操作失败', icon: 'none' }) + } + } catch (err) { + wx.showToast({ title: '操作失败', icon: 'none' }) + } finally { + wx.hideLoading() + } + } + } + }) + }, + + // 跳转到聊天 + goToChat(e) { + const order = e.currentTarget.dataset.order + wx.navigateTo({ + url: `/pages/companion-chat/companion-chat?orderId=${order.id}&userId=${order.user_id}` + }) + }, + + // 跳转到订单详情 + goToDetail(e) { + const orderId = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/order-detail/order-detail?id=${orderId}` + }) + }, + + // 查看评价 + viewReview(e) { + const order = e.currentTarget.dataset.order + if (order.review) { + this.setData({ + currentReview: order.review, + showReplyModal: true, + replyContent: '' + }) + } + }, + + // 关闭回复弹窗 + closeReplyModal() { + this.setData({ + showReplyModal: false, + currentReview: null, + replyContent: '' + }) + }, + + // 输入回复内容 + onReplyInput(e) { + this.setData({ replyContent: e.detail.value }) + }, + + // 提交回复 + async submitReply() { + const { currentReview, replyContent } = this.data + + if (!replyContent.trim()) { + wx.showToast({ title: '请输入回复内容', icon: 'none' }) + return + } + + if (this.data.submittingReply) return + + this.setData({ submittingReply: true }) + wx.showLoading({ title: '提交中...' }) + + try { + const res = await api.companion.replyReview(currentReview.id, replyContent.trim()) + + wx.hideLoading() + this.setData({ submittingReply: false }) + + if (res.success) { + wx.showToast({ title: '回复成功', icon: 'success' }) + this.closeReplyModal() + // 刷新订单列表 + this.loadOrders() + } else { + wx.showToast({ title: res.message || res.error || '回复失败', icon: 'none' }) + } + } catch (err) { + wx.hideLoading() + this.setData({ submittingReply: false }) + console.error('回复评价失败', err) + wx.showToast({ title: '回复失败', icon: 'none' }) + } + } +}) diff --git a/pages/companion-orders/companion-orders.json b/pages/companion-orders/companion-orders.json new file mode 100644 index 0000000..914163f --- /dev/null +++ b/pages/companion-orders/companion-orders.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "我的订单", + "navigationBarBackgroundColor": "#E8C3D4", + "usingComponents": {} +} diff --git a/pages/companion-orders/companion-orders.wxml b/pages/companion-orders/companion-orders.wxml new file mode 100644 index 0000000..648ca4e --- /dev/null +++ b/pages/companion-orders/companion-orders.wxml @@ -0,0 +1,164 @@ + + + var DEFAULT_AVATAR = 'https://ai-c.maimanji.com/images/default-avatar.png'; + module.exports = { + getAvatar: function(avatar) { + return avatar || DEFAULT_AVATAR; + } + }; + + + + + + {{stats.totalOrders || 0}} + 总订单 + + + {{stats.completedOrders || 0}} + 已完成 + + + ¥{{stats.totalIncome || '0.00'}} + 总收入 + + + + + + + 全部 + + + 待服务 + + + 进行中 + + + 已完成 + + + + + + + + + + {{item.statusText}} + + + + + + {{item.serviceTypeText}} + {{item.duration}}分钟 + + + ¥{{item.amount}} + + + + + + + 用户评价 + + + + + {{item.review.content || '用户给了好评'}} + + 我的回复: + {{item.review.reply}} + + 点击回复 + + + + {{item.createTimeText}} + + + + + + + + + + + + + 暂无订单 + + + + + 加载中... + + + 没有更多了 + + + + + + + + 回复评价 + + × + + + + + + + + 用户评分: + + + + + + {{item}} + + {{currentReview.content || '用户给了好评'}} + + + + + 您已回复: + {{currentReview.reply}} + + + + + 输入回复内容 + + {{replyContent.length}}/200 + + + + + + + diff --git a/pages/companion-orders/companion-orders.wxss b/pages/companion-orders/companion-orders.wxss new file mode 100644 index 0000000..6745592 --- /dev/null +++ b/pages/companion-orders/companion-orders.wxss @@ -0,0 +1,488 @@ +/* pages/companion-orders/companion-orders.wxss */ +.container { + min-height: 100vh; + background: linear-gradient(180deg, #E8C3D4 0%, #F5E6EC 100%); + padding-bottom: 40rpx; +} + +/* 统计卡片 */ +.stats-card { + display: flex; + justify-content: space-around; + background: linear-gradient(135deg, #b06ab3 0%, #d4a5d6 100%); + padding: 40rpx 20rpx; + margin: 20rpx; + border-radius: 20rpx; + box-shadow: 0 4rpx 20rpx rgba(176, 106, 179, 0.3); +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; +} + +.stat-value { + font-size: 40rpx; + font-weight: 700; + color: #fff; + margin-bottom: 8rpx; +} + +.stat-label { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.8); +} + +/* 筛选标签 */ +.filter-tabs { + display: flex; + background: #fff; + margin: 0 20rpx 20rpx; + border-radius: 16rpx; + padding: 8rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.tab { + flex: 1; + text-align: center; + padding: 20rpx 0; + font-size: 28rpx; + color: #666; + border-radius: 12rpx; + transition: all 0.3s; +} + +.tab.active { + background: linear-gradient(135deg, #b06ab3 0%, #d4a5d6 100%); + color: #fff; +} + +/* 订单列表 */ +.order-list { + padding: 0 20rpx; +} + +.order-item { + background: #fff; + border-radius: 20rpx; + padding: 24rpx; + margin-bottom: 20rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.order-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20rpx; +} + +.user-info { + display: flex; + align-items: center; +} + +.user-avatar { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + margin-right: 16rpx; +} + +.user-name { + font-size: 30rpx; + font-weight: 500; + color: #333; +} + +.order-status { + padding: 8rpx 20rpx; + border-radius: 20rpx; + font-size: 24rpx; +} + +.order-status.pending { + background: #fff3e0; + color: #ff9800; +} + +.order-status.in_progress { + background: #e3f2fd; + color: #2196f3; +} + +.order-status.completed { + background: #e8f5e9; + color: #4caf50; +} + +.order-status.cancelled { + background: #f5f5f5; + color: #9e9e9e; +} + +.order-content { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16rpx 0; + border-top: 1rpx solid #f5f5f5; + border-bottom: 1rpx solid #f5f5f5; +} + +.order-info { + display: flex; + align-items: center; + gap: 16rpx; +} + +.service-type { + background: #e8c3d4; + color: #b06ab3; + padding: 6rpx 16rpx; + border-radius: 8rpx; + font-size: 24rpx; +} + +.service-duration { + font-size: 26rpx; + color: #666; +} + +.order-price { + font-size: 36rpx; + font-weight: 600; + color: #b06ab3; +} + +.order-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 16rpx; +} + +.order-time { + font-size: 24rpx; + color: #999; +} + +.order-actions { + display: flex; + gap: 16rpx; +} + +.btn-action { + background: linear-gradient(135deg, #b06ab3 0%, #d4a5d6 100%); + color: #fff; + font-size: 24rpx; + padding: 12rpx 24rpx; + border-radius: 30rpx; + border: none; +} + +.btn-action.secondary { + background: #f5f5f5; + color: #666; +} + +/* 空状态 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + padding: 120rpx 0; +} + +.empty-state image { + width: 240rpx; + height: 240rpx; + margin-bottom: 30rpx; + opacity: 0.5; +} + +.empty-state text { + font-size: 30rpx; + color: #999; +} + +/* 加载更多 */ +.load-more { + text-align: center; + padding: 30rpx 0; +} + +.load-more text { + font-size: 26rpx; + color: #999; +} + + +/* ========== 订单评价展示样式 ========== */ +.order-review { + background: #faf5ff; + border-radius: 16rpx; + padding: 20rpx; + margin-top: 16rpx; +} + +.review-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12rpx; +} + +.review-label { + font-size: 24rpx; + color: #6a7282; +} + +.review-rating { + display: flex; + gap: 4rpx; +} + +.review-rating .star { + font-size: 24rpx; +} + +.review-content { + font-size: 26rpx; + color: #364153; + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.review-reply { + margin-top: 12rpx; + padding-top: 12rpx; + border-top: 1rpx solid #e5e7eb; +} + +.reply-label { + font-size: 22rpx; + color: #6a7282; +} + +.reply-text { + font-size: 24rpx; + color: #4a5565; + margin-left: 8rpx; +} + +.reply-btn { + display: inline-block; + margin-top: 12rpx; + font-size: 24rpx; + color: #b06ab3; +} + +/* ========== 回复评价弹窗样式 ========== */ +.reply-modal-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; +} + +.reply-modal { + position: fixed; + left: 0; + right: 0; + bottom: 0; + background: #fff; + border-radius: 32rpx 32rpx 0 0; + z-index: 1001; + max-height: 80vh; + display: flex; + flex-direction: column; + animation: slideUp 0.3s ease; +} + +@keyframes slideUp { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} + +.reply-modal.show { + transform: translateY(0); +} + +.reply-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 32rpx; + border-bottom: 2rpx solid #f3f4f6; +} + +.reply-modal-title { + font-size: 36rpx; + font-weight: 700; + color: #101828; +} + +.reply-modal-close { + width: 48rpx; + height: 48rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.close-icon { + font-size: 48rpx; + color: #6a7282; + line-height: 1; +} + +.reply-modal-content { + flex: 1; + padding: 32rpx; + overflow-y: auto; +} + +/* 评价信息 */ +.review-info { + background: #f9fafb; + border-radius: 16rpx; + padding: 24rpx; + margin-bottom: 24rpx; +} + +.review-rating-row { + display: flex; + align-items: center; + gap: 12rpx; + margin-bottom: 12rpx; +} + +.rating-label { + font-size: 26rpx; + color: #6a7282; +} + +.rating-stars { + display: flex; + gap: 4rpx; +} + +.rating-stars .star { + font-size: 28rpx; +} + +.review-tags { + display: flex; + flex-wrap: wrap; + gap: 12rpx; + margin-bottom: 12rpx; +} + +.review-tags .tag { + background: #fff0f3; + color: #b06ab3; + padding: 6rpx 16rpx; + border-radius: 100rpx; + font-size: 22rpx; +} + +.review-text { + font-size: 28rpx; + color: #364153; + line-height: 1.6; +} + +/* 已有回复 */ +.existing-reply { + background: #ecfdf5; + border-radius: 16rpx; + padding: 24rpx; + margin-bottom: 24rpx; +} + +.existing-reply-label { + font-size: 24rpx; + color: #059669; + display: block; + margin-bottom: 8rpx; +} + +.existing-reply-text { + font-size: 28rpx; + color: #065f46; + line-height: 1.6; +} + +/* 回复输入 */ +.reply-input-section { + margin-bottom: 24rpx; +} + +.input-label { + font-size: 28rpx; + font-weight: 600; + color: #101828; + margin-bottom: 12rpx; + display: block; +} + +.reply-textarea { + width: 100%; + height: 200rpx; + padding: 24rpx; + background: #f9fafb; + border-radius: 16rpx; + font-size: 28rpx; + color: #101828; + box-sizing: border-box; +} + +.char-count { + display: block; + text-align: right; + font-size: 24rpx; + color: #99a1af; + margin-top: 8rpx; +} + +/* 提交按钮 */ +.reply-modal-footer { + padding: 24rpx 32rpx; + padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); + border-top: 2rpx solid #f3f4f6; +} + +.submit-btn { + width: 100%; + height: 96rpx; + background: linear-gradient(135deg, #b06ab3 0%, #d4a5d6 100%); + color: #fff; + font-size: 32rpx; + font-weight: 600; + border-radius: 48rpx; + border: none; + display: flex; + align-items: center; + justify-content: center; +} + +.submit-btn.disabled { + opacity: 0.6; +} + +.submit-btn::after { + border: none; +} diff --git a/pages/cooperation-applications/cooperation-applications.js b/pages/cooperation-applications/cooperation-applications.js new file mode 100644 index 0000000..4730609 --- /dev/null +++ b/pages/cooperation-applications/cooperation-applications.js @@ -0,0 +1,71 @@ +// pages/cooperation-applications/cooperation-applications.js +Page({ + + /** + * 页面的初始数据 + */ + data: { + + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad(options) { + + }, + + /** + * 生命周期函数--监听页面初次渲染完成 + */ + onReady() { + + }, + + /** + * 生命周期函数--监听页面显示 + */ + onShow() { + + }, + + /** + * 生命周期函数--监听页面隐藏 + */ + onHide() { + + }, + + /** + * 生命周期函数--监听页面卸载 + */ + onUnload() { + + }, + + /** + * 页面相关事件处理函数--监听用户下拉动作 + */ + onPullDownRefresh() { + + }, + + /** + * 页面上拉触底事件的处理函数 + */ + onReachBottom() { + + }, + + /** + * 用户点击右上角分享 + */ + onShareAppMessage() { + const referralCode = wx.getStorageSync('referralCode') || '' + const referralCodeParam = referralCode ? `?referralCode=${referralCode}` : '' + return { + title: '合作申请', + path: `/pages/cooperation-applications/cooperation-applications${referralCodeParam}` + } + }, +}) \ No newline at end of file diff --git a/pages/cooperation-applications/cooperation-applications.json b/pages/cooperation-applications/cooperation-applications.json new file mode 100644 index 0000000..8835af0 --- /dev/null +++ b/pages/cooperation-applications/cooperation-applications.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/pages/cooperation-applications/cooperation-applications.wxml b/pages/cooperation-applications/cooperation-applications.wxml new file mode 100644 index 0000000..17506bd --- /dev/null +++ b/pages/cooperation-applications/cooperation-applications.wxml @@ -0,0 +1,2 @@ + +pages/cooperation-applications/cooperation-applications.wxml \ No newline at end of file diff --git a/pages/cooperation-applications/cooperation-applications.wxss b/pages/cooperation-applications/cooperation-applications.wxss new file mode 100644 index 0000000..1283b1d --- /dev/null +++ b/pages/cooperation-applications/cooperation-applications.wxss @@ -0,0 +1 @@ +/* pages/cooperation-applications/cooperation-applications.wxss */ \ No newline at end of file diff --git a/pages/counselor-detail/counselor-detail.js b/pages/counselor-detail/counselor-detail.js new file mode 100644 index 0000000..d50cc10 --- /dev/null +++ b/pages/counselor-detail/counselor-detail.js @@ -0,0 +1,1171 @@ +// pages/counselor-detail/counselor-detail.js - 陪聊师详情页面 +// 对接后端API + +const api = require('../../utils/api') +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, // 总导航栏高度(状态栏+导航内容) + menuButtonTop: 0, + menuButtonHeight: 32, + loading: true, + counselor: null, + messages: [], + currentTime: '', + inputText: '', + inputFocus: false, + // 语音录音相关 + isVoiceMode: false, + isRecording: false, + voiceCancelHint: false, + recordingDuration: 0, + recordingStartY: 0, + // 下单相关 + showOrderModal: false, + selectedDuration: 30, + selectedServiceType: 'text', // 服务类型:text/voice + durations: [ + { value: 15, label: '15分钟' }, + { value: 30, label: '30分钟' }, + { value: 60, label: '60分钟' } + ], + // 人物介绍弹窗 + showProfileModal: false, + // 评价弹窗 + showReviewModal: false, + reviews: [], + reviewStats: { + totalCount: 0, + goodRate: 100 + }, + hasMoreReviews: true, + loadingReviews: false, + reviewPage: 1, + // 更多功能面板 + showMorePanel: false, + // 表情面板 + showEmoji: false, + emojis: [ + "😊", "😀", "😁", "😃", "😂", "🤣", "😅", "😆", "😉", "😋", "😎", "😍", "😘", "🥰", "😗", "😙", + "🙂", "🤗", "🤩", "🤔", "😐", "😑", "😶", "🙄", "😏", "😣", "😥", "😮", "😯", "😪", "😫", "😴", + "🥱", "😌", "😛", "😜", "😝", "🤤", "😒", "😓", "😔", "😕", "🙃", "🤑", "😲", "☹️", "🙁", "😖", + "😞", "😟", "😤", "😢", "😭", "😦", "😧", "😨", "😩", "🤯", "😬", "😰", "😱", "🥵", "🥶", "😳", + "🤪", "😵", "🥴", "😠", "😡", "🤬", "😷", "🤒", "🤕", "🤢", "🤮", "🤧", "😇", "🥳", "🥺", "🤠", + "❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "🤎", "💔", "❣️", "💕", "💞", "💓", "💗", "💖", + "💘", "💝", "💟", "👍", "👎", "👏", "🙌", "👐", "🤲", "🤝", "🙏", "✌️", "🤞", "🤟", "🤘", "👌" + ] + }, + + onLoad(options) { + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + + // 获取胶囊按钮位置信息 + const menuButton = wx.getMenuButtonBoundingClientRect() + + // 正确计算导航栏高度的方法: + // 导航内容高度 = 胶囊按钮高度 + 上下边距 + // 胶囊按钮距离状态栏的间距 = menuButton.top - statusBarHeight + // 导航内容高度 = 胶囊按钮高度 + 2 * 间距(上下对称) + const navContentHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + + // 整个导航栏高度(包含状态栏)= 状态栏高度 + 导航内容高度 + const totalNavHeight = statusBarHeight + navContentHeight + + // 获取当前时间 + const now = new Date() + const hours = now.getHours() + const minutes = now.getMinutes().toString().padStart(2, '0') + const period = hours < 12 ? '上午' : (hours < 18 ? '下午' : '晚上') + const displayHour = hours > 12 ? hours - 12 : hours + + this.setData({ + statusBarHeight, + navBarHeight: navContentHeight, // 导航内容高度(不含状态栏) + totalNavHeight, // 总高度(含状态栏) + menuButtonTop: menuButton.top, + menuButtonHeight: menuButton.height, + currentTime: `${period} ${displayHour}:${minutes}` + }) + + // 加载陪聊师数据 + if (options.id) { + this.loadCounselorDetail(options.id) + } else { + wx.showToast({ title: '参数错误', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + } + }, + + /** + * 页面卸载时清理资源 + */ + onUnload() { + // 停止录音 + if (this.data.isRecording && this.recorderManager) { + this.voiceCanceled = true + this.recorderManager.stop() + } + + // 清除录音计时器 + if (this.recordingTimer) { + clearInterval(this.recordingTimer) + this.recordingTimer = null + } + + // 清理录音管理器 + if (this.recorderManager) { + this.recorderManager = null + } + }, + + /** + * 页面隐藏时 + */ + onHide() { + // 停止录音 + if (this.data.isRecording && this.recorderManager) { + this.voiceCanceled = true + this.recorderManager.stop() + this.setData({ isRecording: false }) + } + }, + + /** + * 加载陪聊师详情 + */ + async loadCounselorDetail(id) { + this.setData({ loading: true }) + + try { + console.log('加载陪聊师详情,ID:', id) + const res = await api.companion.getDetail(id) + + console.log('陪聊师详情响应:', res) + + // 兼容两种返回格式:{ success: true, data: {...} } 或 { code: 0, data: {...} } + if ((res.success || res.code === 0) && res.data) { + const counselor = this.transformCounselor(res.data) + + console.log('转换后的陪聊师数据:', counselor) + + // 设置欢迎消息 + const welcomeMsg = { + id: 1, + content: `您好,我是${counselor.name}。很高兴在这里陪伴您。有什么可以帮助您的吗?【系统自动回复】` + } + + this.setData({ + counselor, + messages: [welcomeMsg], + loading: false + }) + } else { + console.error('API返回失败:', res.error || res.message) + wx.showToast({ title: res.error || '加载失败', icon: 'none' }) + this.setData({ loading: false }) + setTimeout(() => wx.navigateBack(), 1500) + } + } catch (err) { + console.error('加载陪聊师详情失败', err) + wx.showToast({ title: '网络错误', icon: 'none' }) + this.setData({ loading: false }) + setTimeout(() => wx.navigateBack(), 1500) + } + }, + + /** + * 加载模拟陪聊师数据 + */ + loadMockCounselor(id) { + const mockData = { + 'c001': { + id: 'c001', + name: '林心怡', + avatarColor: '#e8b4d8', + avatarColorEnd: '#c984cd', + location: '北京', + city: '北京', + age: '28岁', + experience: '5年咨询经验', + education: '心理学硕士', + serviceCount: 1286, + repeatCount: 423, + rating: 4.96, + quote: '每一次倾诉,都是心灵的释放', + certification: '国家二级心理咨询师 | 情感咨询专家认证', + status: 'online', + statusText: '在线', + isBusy: false, + levelCode: 'junior', + levelName: '初级', + textPrice: 0.5, + voicePrice: 1 + }, + 'c002': { + id: 'c002', + name: '张明辉', + avatarColor: '#a8d8ea', + avatarColorEnd: '#6bb3d9', + location: '上海', + city: '上海', + age: '35岁', + experience: '8年咨询经验', + education: '应用心理学博士', + serviceCount: 2156, + repeatCount: 687, + rating: 4.92, + quote: '用专业的态度,温暖每一颗心', + certification: '高级心理咨询师 | 职场心理专家', + status: 'online', + statusText: '在线', + isBusy: false, + levelCode: 'senior', + levelName: '高级', + textPrice: 1.5, + voicePrice: 3 + }, + 'c003': { + id: 'c003', + name: '王雨萱', + avatarColor: '#f8c8dc', + avatarColorEnd: '#e89bb8', + location: '深圳', + city: '深圳', + age: '26岁', + experience: '3年咨询经验', + education: '心理学学士', + serviceCount: 856, + repeatCount: 298, + rating: 4.89, + quote: '倾听你的故事,陪伴你的成长', + certification: '情感咨询师 | 青年心理辅导员', + status: 'offline', + statusText: '离线', + isBusy: true, + levelCode: 'intermediate', + levelName: '中级', + textPrice: 1, + voicePrice: 2 + }, + 'c004': { + id: 'c004', + name: '李思远', + avatarColor: '#b8d4e3', + avatarColorEnd: '#8ab4cf', + location: '广州', + city: '广州', + age: '42岁', + experience: '15年咨询经验', + education: '临床心理学硕士', + serviceCount: 3421, + repeatCount: 1156, + rating: 4.98, + quote: '专业倾听,用心陪伴每一刻', + certification: '资深心理治疗师 | 家庭治疗师认证', + status: 'online', + statusText: '在线', + isBusy: false, + levelCode: 'expert', + levelName: '资深', + textPrice: 2, + voicePrice: 4 + }, + 'c005': { + id: 'c005', + name: '陈晓琳', + avatarColor: '#d4b8e8', + avatarColorEnd: '#b088d4', + location: '杭州', + city: '杭州', + age: '31岁', + experience: '6年咨询经验', + education: '发展心理学硕士', + serviceCount: 1567, + repeatCount: 512, + rating: 4.94, + quote: '让每一次对话都充满温暖', + certification: '心理咨询师 | 情绪管理专家', + status: 'online', + statusText: '在线', + isBusy: false, + levelCode: 'senior', + levelName: '高级', + textPrice: 1.5, + voicePrice: 3 + }, + 'c006': { + id: 'c006', + name: '赵文博', + avatarColor: '#a8e6cf', + avatarColorEnd: '#7bc9a6', + location: '成都', + city: '成都', + age: '38岁', + experience: '10年咨询经验', + education: '社会心理学博士', + serviceCount: 1892, + repeatCount: 634, + rating: 4.91, + quote: '理性分析,感性陪伴', + certification: '高级心理顾问 | 企业EAP咨询师', + status: 'busy', + statusText: '忙碌中', + isBusy: true, + levelCode: 'intermediate', + levelName: '中级', + textPrice: 1, + voicePrice: 2 + } + } + + const counselor = mockData[id] || mockData['c001'] + + // 设置欢迎消息 + const welcomeMsg = { + id: 1, + content: `您好,我是${counselor.name}。很高兴在这里陪伴您。有什么可以帮助您的吗?【系统自动回复】` + } + + this.setData({ + counselor, + messages: [welcomeMsg], + loading: false + }) + }, + + /** + * 转换陪聊师数据格式 + */ + transformCounselor(data) { + const config = require('../../config/index') + console.log('原始陪聊师数据:', data) + + const statusMap = { + online: { text: '在线', isBusy: false }, + busy: { text: '忙碌中', isBusy: true }, + offline: { text: '离线', isBusy: true } + } + + const status = data.status || data.onlineStatus || data.online_status || 'offline' + const statusInfo = statusMap[status] || statusMap.offline + + // 等级名称映射 + const levelNameMap = { + junior: '初级', + intermediate: '中级', + senior: '高级', + expert: '资深' + } + + // 优先使用 displayName,然后是 name,最后是 nickname + const name = data.displayName || data.display_name || data.name || data.nickname || '未知' + + // 处理地址,只显示城市名 + let location = data.location || data.city || '' + if (location) { + // 如果地址包含多个部分(如"北京 北京市 朝阳区"),只取第一个城市名 + const parts = location.split(/[\s,,]+/).filter(p => p.trim()) + if (parts.length > 0) { + location = parts[0].replace(/[省市区县]$/, '') || parts[0] + } + } + + // 处理头像URL - 如果是相对路径,拼接完整域名 + let avatar = data.avatar || '' + if (avatar && avatar.startsWith('/')) { + // 从 API_BASE_URL 提取域名(去掉 /api 后缀) + const baseUrl = config.API_BASE_URL.replace(/\/api$/, '') + avatar = baseUrl + avatar + } + + const result = { + id: data.id, + name: name, + avatarColor: data.avatar_color || data.avatarColor || '#c984cd', + avatarColorEnd: data.avatar_color_end || data.avatarColorEnd || '#b06ab3', + avatar: avatar, + location: location, + city: location, + age: data.age_group || data.ageGroup || data.age || '', + experience: data.experience || '', + education: data.education || '', + serviceCount: data.service_count || data.serviceCount || data.totalOrders || data.total_orders || 0, + repeatCount: data.repeat_count || data.repeatCount || 0, + rating: data.rating || 5.0, + quote: data.quote || data.bio || data.introduction || '', + certification: data.certification || '', + status: status, + statusText: data.statusText || statusInfo.text, + isBusy: data.isBusy !== undefined ? data.isBusy : statusInfo.isBusy, + // 等级信息 + levelCode: data.levelCode || data.level_code || 'junior', + levelName: data.levelName || data.level_name || levelNameMap[data.levelCode || data.level_code] || '初级', + // 基于等级的价格 + textPrice: data.textPrice || data.text_price || data.pricePerMinute || data.price_per_minute || 0.5, + voicePrice: data.voicePrice || data.voice_price || 1 + } + + console.log('转换后的陪聊师数据:', result) + return result + }, + + onBack() { + wx.navigateBack() + }, + + onMore() { + wx.showActionSheet({ + itemList: ['举报', '拉黑', '分享'], + success: (res) => { + const actions = ['举报', '拉黑', '分享'] + if (res.tapIndex === 1) { + // 拉黑 + this.addToBlacklist() + } else { + wx.showToast({ title: actions[res.tapIndex], icon: 'none' }) + } + } + }) + }, + + /** + * 添加到黑名单 + */ + async addToBlacklist() { + try { + const res = await api.settings.addToBlacklist(this.data.counselor.id) + if (res.success) { + wx.showToast({ title: '已拉黑', icon: 'success' }) + } + } catch (err) { + wx.showToast({ title: '操作失败', icon: 'none' }) + } + }, + + /** + * 免费倾诉/下单 + */ + onFreeConsult() { + const { counselor } = this.data + + if (counselor.isBusy) { + wx.showToast({ title: '陪聊师当前不在线', icon: 'none' }) + return + } + + // 显示下单弹窗 + this.setData({ showOrderModal: true }) + }, + + /** + * 关闭下单弹窗 + */ + closeOrderModal() { + this.setData({ showOrderModal: false }) + }, + + /** + * 选择时长 + */ + selectDuration(e) { + const duration = e.currentTarget.dataset.duration + this.setData({ selectedDuration: duration }) + }, + + /** + * 选择服务类型 + */ + selectServiceType(e) { + const type = e.currentTarget.dataset.type + this.setData({ selectedServiceType: type }) + }, + + /** + * 计算订单价格 + */ + calculatePrice() { + const { counselor, selectedDuration, selectedServiceType } = this.data + if (!counselor) return 0 + + const unitPrice = selectedServiceType === 'voice' ? counselor.voicePrice : counselor.textPrice + return (unitPrice * selectedDuration).toFixed(2) + }, + + /** + * 确认下单 + */ + async confirmOrder() { + const { counselor, selectedDuration, selectedServiceType } = this.data + + // 检查登录 + if (app.checkNeedLogin && app.checkNeedLogin()) return + + wx.showLoading({ title: '创建订单...' }) + + try { + const res = await api.order.createCompanionOrder({ + companion_id: counselor.id, + duration: selectedDuration, + service_type: selectedServiceType, + message: '' + }) + + wx.hideLoading() + + if (res.success && res.data) { + this.setData({ showOrderModal: false }) + + // 跳转到陪聊聊天页 + wx.navigateTo({ + url: `/pages/companion-chat/companion-chat?orderId=${res.data.id}&companionId=${counselor.id}&name=${encodeURIComponent(counselor.name)}` + }) + } else { + wx.showToast({ title: res.message || '下单失败', icon: 'none' }) + } + } catch (err) { + wx.hideLoading() + console.error('下单失败', err) + wx.showToast({ title: '下单失败', icon: 'none' }) + } + }, + + onViewProfile() { + this.setData({ showProfileModal: true }) + }, + + closeProfileModal() { + this.setData({ showProfileModal: false }) + }, + + onViewReviews() { + this.setData({ + showReviewModal: true, + reviews: [], + reviewPage: 1, + hasMoreReviews: true + }) + this.loadReviews() + }, + + closeReviewModal() { + this.setData({ showReviewModal: false }) + }, + + /** + * 加载评价列表 + */ + async loadReviews() { + if (this.data.loadingReviews || !this.data.hasMoreReviews) return + + this.setData({ loadingReviews: true }) + + try { + const res = await api.companion.getReviews(this.data.counselor.id, { + page: this.data.reviewPage, + limit: 10 + }) + + if (res.success && res.data) { + const newReviews = res.data.reviews || res.data.list || [] + const formattedReviews = newReviews.map(review => ({ + ...review, + userName: this.maskPhone(review.userName || review.user_name || '匿名用户'), + createdAt: this.formatDate(review.createdAt || review.created_at), + expanded: false + })) + + // 获取统计数据(兼容多种返回格式) + const stats = res.data.stats || {} + const totalCount = stats.reviewCount || res.data.totalCount || res.data.total || formattedReviews.length + const goodRate = stats.goodRate || res.data.goodRate || 100 + const avgRating = stats.avgRating || res.data.avgRating || this.data.counselor?.rating || 5 + + this.setData({ + reviews: [...this.data.reviews, ...formattedReviews], + reviewStats: { + totalCount: totalCount, + goodRate: goodRate, + avgRating: avgRating + }, + hasMoreReviews: res.data.hasMore !== undefined ? res.data.hasMore : formattedReviews.length >= 10, + reviewPage: this.data.reviewPage + 1, + loadingReviews: false + }) + } else { + // 使用模拟数据 + this.loadMockReviews() + } + } catch (err) { + console.error('加载评价失败', err) + this.loadMockReviews() + } + }, + + /** + * 加载模拟评价数据 + */ + loadMockReviews() { + const mockReviews = [ + { + id: 1, + userName: '138****6172', + userAvatar: '', + rating: 5, + content: '老师很有耐心,倾听我的问题后给出了很中肯的建议。咨询后感觉心里轻松了很多,对未来也有了新的规划...', + tags: ['专业', '耐心', '有效果'], + reply: '谢谢您的信任,很高兴能够帮助到您。希望您能继续保持积极的心态,有任何问题随时可以来找我交流。', + likeCount: 23, + createdAt: '2024-12-15 14:32' + }, + { + id: 2, + userName: '186****3298', + userAvatar: '', + rating: 5, + content: '第一次尝试心理咨询,老师非常专业,让我感觉很放松。通过几次咨询,我对自己的情绪有了更好的认识...', + tags: ['专业', '温暖', '有帮助'], + reply: '能够陪伴您成长是我的荣幸,继续加油!', + likeCount: 15, + createdAt: '2024-12-10 09:15' + }, + { + id: 3, + userName: '159****7721', + userAvatar: '', + rating: 5, + content: '老师的声音很温柔,聊天的过程中感觉很舒服。虽然问题还在,但是心态好了很多,会继续找老师咨询的...', + tags: ['温柔', '善于倾听'], + reply: '', + likeCount: 8, + createdAt: '2024-12-05 20:48' + }, + { + id: 4, + userName: '177****4532', + userAvatar: '', + rating: 5, + content: '咨询师很专业,能够快速理解我的问题并给出建议。性价比很高,会推荐给朋友...', + tags: ['专业', '高效'], + reply: '', + likeCount: 12, + createdAt: '2024-11-28 16:22' + }, + { + id: 5, + userName: '133****8965', + userAvatar: '', + rating: 5, + content: '非常好的一次体验,老师很有同理心,让我感受到了被理解和支持...', + tags: ['有同理心', '支持'], + reply: '感谢您的认可,祝您生活愉快!', + likeCount: 6, + createdAt: '2024-11-20 11:05' + } + ] + + this.setData({ + reviews: mockReviews, + reviewStats: { + totalCount: this.data.counselor?.serviceCount || 2952, + goodRate: 100 + }, + hasMoreReviews: false, + loadingReviews: false + }) + }, + + /** + * 加载更多评价 + */ + loadMoreReviews() { + this.loadReviews() + }, + + /** + * 展开评价内容 + */ + expandReview(e) { + const index = e.currentTarget.dataset.index + const reviews = this.data.reviews + reviews[index].expanded = true + this.setData({ reviews }) + }, + + /** + * 点赞评价 + */ + async likeReview(e) { + const reviewId = e.currentTarget.dataset.id + const reviews = this.data.reviews + const index = reviews.findIndex(r => r.id === reviewId) + + if (index !== -1) { + // 检查是否已点赞 + if (reviews[index].liked) { + wx.showToast({ title: '已点赞过了', icon: 'none' }) + return + } + + // 乐观更新UI + reviews[index].likeCount = (reviews[index].likeCount || 0) + 1 + reviews[index].liked = true + this.setData({ reviews }) + + // 调用API + try { + const res = await api.companion.likeReview(reviewId) + if (!res.success) { + // 失败时回滚 + reviews[index].likeCount -= 1 + reviews[index].liked = false + this.setData({ reviews }) + wx.showToast({ title: '点赞失败', icon: 'none' }) + } + } catch (err) { + // 失败时回滚 + reviews[index].likeCount -= 1 + reviews[index].liked = false + this.setData({ reviews }) + console.log('点赞API调用失败', err) + wx.showToast({ title: '点赞失败', icon: 'none' }) + } + } + }, + + /** + * 手机号脱敏 + */ + maskPhone(phone) { + if (!phone || phone.length < 7) return phone + if (phone.includes('****')) return phone + return phone.substring(0, 3) + '****' + phone.substring(phone.length - 4) + }, + + /** + * 格式化日期 + */ + formatDate(dateStr) { + if (!dateStr) return '' + const date = new Date(dateStr) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + return `${year}-${month}-${day} ${hours}:${minutes}` + }, + + onRemind() { + wx.showToast({ title: '已设置提醒', icon: 'success' }) + }, + + onVoiceMode() { + const isVoiceMode = !this.data.isVoiceMode + this.setData({ + isVoiceMode, + inputFocus: !isVoiceMode + }) + }, + + /** + * 语音按钮触摸开始 - 开始录音 + */ + onVoiceTouchStart(e) { + // 记录起始Y坐标 + const startY = e.touches[0].clientY + this.setData({ + recordingStartY: startY, + voiceCancelHint: false, + recordingDuration: 0 + }) + + // 检查录音权限 + wx.authorize({ + scope: 'scope.record', + success: () => { + // 开始录音 + this.startVoiceRecord() + }, + fail: () => { + wx.showModal({ + title: '需要录音权限', + content: '请在设置中开启录音权限', + confirmText: '去设置', + success: (res) => { + if (res.confirm) { + wx.openSetting() + } + } + }) + } + }) + }, + + /** + * 语音按钮触摸移动 - 检测上划取消 + */ + onVoiceTouchMove(e) { + if (!this.data.isRecording) return + + const currentY = e.touches[0].clientY + const startY = this.data.recordingStartY + const moveDistance = startY - currentY + + // 上划超过80px显示取消提示 + const shouldCancel = moveDistance > 80 + + if (shouldCancel !== this.data.voiceCancelHint) { + this.setData({ voiceCancelHint: shouldCancel }) + + // 震动反馈 + if (shouldCancel) { + wx.vibrateShort({ type: 'light' }) + } + } + }, + + /** + * 语音按钮触摸结束 - 停止录音并发送/取消 + */ + onVoiceTouchEnd() { + if (!this.data.isRecording) return + + const { voiceCancelHint } = this.data + + // 标记是否取消 + this.voiceCanceled = voiceCancelHint + + // 停止录音 + if (this.recorderManager) { + this.recorderManager.stop() + } + + // 清除录音计时器 + if (this.recordingTimer) { + clearInterval(this.recordingTimer) + this.recordingTimer = null + } + + this.setData({ + isRecording: false, + voiceCancelHint: false, + recordingDuration: 0 + }) + }, + + /** + * 语音按钮触摸取消 + */ + onVoiceTouchCancel() { + this.voiceCanceled = true + this.onVoiceTouchEnd() + }, + + /** + * 开始语音录音 + */ + startVoiceRecord() { + this.setData({ + isRecording: true, + voiceCancelHint: false, + recordingDuration: 0 + }) + + // 初始化录音管理器 + const recorderManager = wx.getRecorderManager() + this.recorderManager = recorderManager + + // 监听录音结束 + recorderManager.onStop((res) => { + // 清除计时器 + if (this.recordingTimer) { + clearInterval(this.recordingTimer) + this.recordingTimer = null + } + + this.setData({ isRecording: false }) + + // 如果是取消的,不发送 + if (this.voiceCanceled) { + this.voiceCanceled = false + wx.showToast({ title: '已取消', icon: 'none' }) + return + } + + // 录音时间太短 + if (res.duration < 1000) { + wx.showToast({ title: '录音时间太短', icon: 'none' }) + return + } + + // 发送语音消息 + this.sendVoiceMessage(res.tempFilePath, Math.ceil(res.duration / 1000)) + }) + + recorderManager.onError((err) => { + console.error('录音失败', err) + + // 清除计时器 + if (this.recordingTimer) { + clearInterval(this.recordingTimer) + this.recordingTimer = null + } + + this.setData({ + isRecording: false, + voiceCancelHint: false, + recordingDuration: 0 + }) + + // 模拟器不支持录音,给出友好提示 + if (err.errMsg && err.errMsg.includes('NotFoundError')) { + wx.showToast({ title: '请在真机上测试录音', icon: 'none' }) + } else { + wx.showToast({ title: '录音失败', icon: 'none' }) + } + }) + + // 开始录音 + recorderManager.start({ + duration: 60000, + format: 'mp3', + sampleRate: 16000, + numberOfChannels: 1 + }) + + // 录音计时器 + this.recordingTimer = setInterval(() => { + const duration = this.data.recordingDuration + 1 + this.setData({ recordingDuration: duration }) + + // 最长60秒自动停止 + if (duration >= 60) { + this.onVoiceTouchEnd() + } + }, 1000) + }, + + /** + * 发送语音消息 + */ + async sendVoiceMessage(filePath, duration) { + const { counselor, messages } = this.data + + // 添加语音消息到列表 + const voiceMessage = { + id: Date.now(), + content: `[语音消息 ${duration}″]`, + isUser: true, + type: 'voice', + audioUrl: filePath, + duration: duration + } + + this.setData({ + messages: [...messages, voiceMessage] + }) + + wx.showToast({ title: '语音已发送', icon: 'success' }) + + // 如果陪聊师在线,提示开始对话 + if (!counselor.isBusy) { + setTimeout(() => { + wx.showModal({ + title: '提示', + content: '是否开始与陪聊师对话?', + confirmText: '开始对话', + cancelText: '继续留言', + success: (res) => { + if (res.confirm) { + this.onFreeConsult() + } + } + }) + }, 500) + } + + // TODO: 上传语音文件到服务器 + }, + + onInput(e) { + this.setData({ inputText: e.detail.value }) + }, + + /** + * 发送消息 + */ + onSendMessage() { + const { inputText, messages, counselor } = this.data + if (!inputText.trim()) return + + // 添加用户消息 + const userMsg = { + id: Date.now(), + content: inputText, + isUser: true + } + + this.setData({ + messages: [...messages, userMsg], + inputText: '' + }) + + // 如果陪聊师在线,可以跳转到聊天页面 + if (!counselor.isBusy) { + wx.showModal({ + title: '提示', + content: '是否开始与陪聊师对话?', + confirmText: '开始对话', + cancelText: '继续留言', + success: (res) => { + if (res.confirm) { + this.onFreeConsult() + } + } + }) + } + }, + + onEmoji() { + // 切换表情面板 + this.setData({ + showEmoji: !this.data.showEmoji, + showMorePanel: false, + isVoiceMode: false + }) + }, + + /** + * 选择表情 + */ + onEmojiSelect(e) { + const emoji = e.currentTarget.dataset.emoji + this.setData({ + inputText: this.data.inputText + emoji + }) + }, + + /** + * 删除表情/文字 + */ + onEmojiDelete() { + const text = this.data.inputText + if (text.length > 0) { + // 处理emoji字符(可能占用多个字符位置) + const arr = Array.from(text) + arr.pop() + this.setData({ + inputText: arr.join('') + }) + } + }, + + onAdd() { + // 切换更多功能面板显示状态 + this.setData({ + showMorePanel: !this.data.showMorePanel, + showEmoji: false, + isVoiceMode: false + }) + }, + + // 关闭更多功能面板和表情面板 + closeMorePanel() { + this.setData({ + showMorePanel: false, + showEmoji: false + }) + }, + + // 拍照 + onTakePhoto() { + this.setData({ showMorePanel: false }) + wx.chooseMedia({ + count: 1, + mediaType: ['image'], + sourceType: ['camera'], + camera: 'back', + success: (res) => { + const tempFilePath = res.tempFiles[0].tempFilePath + wx.showToast({ title: '照片已选择', icon: 'success' }) + // TODO: 发送图片消息 + }, + fail: (err) => { + if (err.errMsg !== 'chooseMedia:fail cancel') { + wx.showToast({ title: '拍照失败', icon: 'none' }) + } + } + }) + }, + + // 从相册选择图片 + onChooseImage() { + this.setData({ showMorePanel: false }) + wx.chooseMedia({ + count: 9, + mediaType: ['image'], + sourceType: ['album'], + success: (res) => { + wx.showToast({ title: `已选择${res.tempFiles.length}张图片`, icon: 'success' }) + // TODO: 发送图片消息 + }, + fail: (err) => { + if (err.errMsg !== 'chooseMedia:fail cancel') { + wx.showToast({ title: '选择图片失败', icon: 'none' }) + } + } + }) + }, + + // 发送礼物 + onSendGift() { + this.setData({ showMorePanel: false }) + wx.showToast({ title: '礼物功能开发中', icon: 'none' }) + }, + + // 语音通话 + onVoiceCall() { + this.setData({ showMorePanel: false }) + wx.showToast({ title: '语音通话功能开发中', icon: 'none' }) + }, + + // 常用语 + onQuickReply() { + this.setData({ showMorePanel: false }) + const quickReplies = [ + '你好,很高兴认识你~', + '最近怎么样?', + '有什么想聊的吗?', + '今天心情如何?', + '晚安,好梦~' + ] + wx.showActionSheet({ + itemList: quickReplies, + success: (res) => { + this.setData({ inputText: quickReplies[res.tapIndex] }) + } + }) + }, + + // 约时间 + onScheduleTime() { + this.setData({ showMorePanel: false }) + wx.showToast({ title: '约时间功能开发中', icon: 'none' }) + }, + + // 抢红包 + onRedPacket() { + this.setData({ showMorePanel: false }) + wx.showToast({ title: '红包功能开发中', icon: 'none' }) + }, + + // 测结果 + onTestResult() { + this.setData({ showMorePanel: false }) + wx.showToast({ title: '测结果功能开发中', icon: 'none' }) + } +}) diff --git a/pages/counselor-detail/counselor-detail.json b/pages/counselor-detail/counselor-detail.json new file mode 100644 index 0000000..004b603 --- /dev/null +++ b/pages/counselor-detail/counselor-detail.json @@ -0,0 +1,5 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "", + "navigationStyle": "custom" +} diff --git a/pages/counselor-detail/counselor-detail.wxml b/pages/counselor-detail/counselor-detail.wxml new file mode 100644 index 0000000..6fd199c --- /dev/null +++ b/pages/counselor-detail/counselor-detail.wxml @@ -0,0 +1,525 @@ + + + + + + + + + + {{counselor.name}} + + {{counselor.statusText}} + + + + + + + + + + + + + + + + {{counselor.name[0]}} + + + + + + {{counselor.name}} + + + {{counselor.levelName}} + + + + + {{counselor.location}} + + + {{counselor.city}} | {{counselor.age}} | {{counselor.experience}} + {{counselor.education}} + + + + + + {{counselor.serviceCount}} + 服务人次 + + + {{counselor.repeatCount}} + 回头客 + + + {{counselor.rating}} + 评分 + + + + + 聆听寄语 + {{counselor.quote}} + + + 专业资质 + {{counselor.certification}} + + + + + + + {{currentTime}} + + + + + + + + + + {{counselor.name[0]}} + + + + {{item.content}} + + + + + + + 咨询师正在服务中,无法及时回复,您可留言 + + 【空闲时提醒我】 + + + + + + + + + + + + + + 免费倾诉 + + + 📄 + 人物介绍 + + + 💬 + 查看评价 + + + + + + + + + + + + + + + + + + + {{isRecording ? (voiceCancelHint ? '松开取消' : '松开发送') : '按住 说话'}} + + + + + + + + + + + + + + + + + + + {{item}} + + + + + + + + + 发送 + + + + + + + + + + + + + + + 拍照 + + + + + + + + + + + 相册 + + + + + + + 礼物 + + + + + + + 语音 + + + + + + + + + + + + + 常用语 + + + + + + + 约时间 + + + + + + + + + + + 抢红包 + + + + + + NEW + + 测结果 + + + + + + + + + + + + + + + + + {{voiceCancelHint ? '松开手指,取消发送' : '手指上划,取消发送'}} + {{recordingDuration}}″ + + + + + + + + + + + + + + 选择服务 + × + + + + + + + + 文字聊天 + ¥{{counselor.textPrice}}/分钟 + + + 语音聊天 + ¥{{counselor.voicePrice}}/分钟 + + + + + + + + + + {{item.label}} + + + + + + + 预计费用 + ¥{{(selectedServiceType === 'voice' ? counselor.voicePrice : counselor.textPrice) * selectedDuration}} + + + + + + + + + + + 人物介绍 + + × + + + + + + + + + + + {{counselor.name[0]}} + + + + {{counselor.name}} + + + 性别: + {{counselor.gender || '女'}} + + + 年龄: + {{counselor.age || '90后'}} + + + 地区: + {{counselor.city || counselor.location || '未知'}} + + + 学历: + {{counselor.education || '本科'}} + + + + + + + + + + {{counselor.serviceCount || 0}} + 服务人次 + + + + {{counselor.repeatCount || 0}} + 回头客 + + + + {{counselor.rating || 5.0}} + 评分 + + + + + + + 专业资质 + {{counselor.certification || '心理咨询师'}} + + + + + 擅长领域 + + + {{item}} + + + + + + + 个人介绍 + {{counselor.introduction || counselor.experience || '90后 本科 心理咨询师1年系统训练'}} + + + + + 聆听寄语 + "{{counselor.quote || '生活中的每一份情绪都值得被温柔对待,我愿意陪伴您度过人生的每一个重要时刻。'}}" + + + + + + + + + + 全部评价 + + × + + + + + + + + + {{counselor.name[0]}} + + + + + {{counselor.rating || 4.9}} + + + + + + {{reviewStats.totalCount || 0}}条评价 + + + + {{reviewStats.goodRate || 100}}% + 好评率 + + + + + + + + + + + + {{item.userName[0] || '用'}} + + + + + + + + {{item.content}} + 展开 + + + + + + {{tag}} + + + + + + + 咨询师回复: + {{item.reply}} + + + + + + + + + + + + 已经到底了~ + + + + + 加载中... + + + diff --git a/pages/counselor-detail/counselor-detail.wxss b/pages/counselor-detail/counselor-detail.wxss new file mode 100644 index 0000000..5c717f5 --- /dev/null +++ b/pages/counselor-detail/counselor-detail.wxss @@ -0,0 +1,1738 @@ +/* 陪聊师详情页面样式 */ +page { + background: #ededed; + height: 100%; +} + +.page-container { + height: 100vh; + display: flex; + flex-direction: column; + position: relative; +} + +.status-bar { + background: #fff; +} + +/* 顶部导航栏 - 固定定位 */ +.nav-bar { + position: fixed; + top: 0; + left: 0; + right: 0; + background: #fff; + border-bottom: 2rpx solid #f3f4f6; + z-index: 100; + box-sizing: border-box; +} + +.nav-content { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 32rpx; + height: 100%; +} + +.nav-back { + width: 64rpx; + height: 64rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.nav-placeholder { + width: 64rpx; + height: 64rpx; +} + +.back-icon { + width: 48rpx; + height: 48rpx; +} + +.nav-title-wrap { + display: flex; + align-items: center; + gap: 16rpx; +} + +.nav-title { + font-size: 32rpx; + color: #101828; +} + +.status-badge { + padding: 4rpx 16rpx; + border-radius: 8rpx; + background: #ff8904; +} + +.status-badge.online { + background: #05df72; +} + +.status-badge.busy { + background: #ff8904; +} + +.status-text { + font-size: 24rpx; + color: #fff; +} + +/* 内容区域 */ +.content-scroll { + flex: 1; + padding: 16rpx 32rpx; + padding-bottom: 450rpx; /* 为底部固定区域留出空间 */ + box-sizing: border-box; +} + +/* 用户信息卡片 */ +.profile-card { + background: #fff; + border-radius: 32rpx; + overflow: hidden; + box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1), 0 2rpx 4rpx rgba(0, 0, 0, 0.1); +} + +.profile-header { + display: flex; + gap: 24rpx; + padding: 32rpx; + background: linear-gradient(152deg, #fce7f3 0%, #f3e8ff 50%, #e9d4ff 100%); +} + +.profile-avatar-wrap { + width: 128rpx; + height: 160rpx; +} + +.profile-avatar-placeholder { + width: 128rpx; + height: 160rpx; + border-radius: 20rpx; + display: flex; + align-items: center; + justify-content: center; +} + +/* 真实头像图片样式 */ +.profile-avatar-image { + width: 128rpx; + height: 160rpx; + border-radius: 20rpx; +} + +.profile-avatar-text { + font-size: 48rpx; + font-weight: 700; + color: #fff; +} + +.profile-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.profile-name-row { + display: flex; + align-items: center; + justify-content: space-between; +} + +.name-level-wrap { + display: flex; + align-items: center; + gap: 12rpx; +} + +.profile-name { + font-size: 36rpx; + font-weight: 700; + color: #101828; +} + +/* 名字后面的等级标签 */ +.name-level-badge { + padding: 4rpx 16rpx; + border-radius: 16rpx; + font-size: 20rpx; + white-space: nowrap; +} + +.name-level-badge.level-junior { + background: linear-gradient(135deg, #a8d8ea 0%, #6bb3d9 100%); +} + +.name-level-badge.level-intermediate { + background: linear-gradient(135deg, #b8e986 0%, #7bc96f 100%); +} + +.name-level-badge.level-senior { + background: linear-gradient(135deg, #ffd700 0%, #ffb347 100%); +} + +.name-level-badge.level-expert { + background: linear-gradient(135deg, #e8b4d8 0%, #c984cd 100%); +} + +.name-level-text { + color: #fff; + font-weight: 600; + font-size: 22rpx; +} + +.profile-location { + display: flex; + align-items: center; + gap: 8rpx; +} + +.location-icon { + width: 24rpx; + height: 24rpx; +} + +.location-text { + font-size: 24rpx; + color: #6a7282; +} + +.profile-desc { + font-size: 28rpx; + color: #4a5565; +} + +.profile-education { + font-size: 28rpx; + color: #4a5565; +} + +.profile-stats { + display: flex; + justify-content: space-around; + padding: 24rpx 32rpx; + background: linear-gradient(152deg, #fce7f3 0%, #f3e8ff 50%, #e9d4ff 100%); + border-top: 2rpx solid rgba(255, 255, 255, 0.3); +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 4rpx; +} + +.stat-value { + font-size: 40rpx; + font-weight: 700; + color: #101828; +} + +.stat-label { + font-size: 24rpx; + color: #6a7282; +} + +.profile-details { + padding: 24rpx 32rpx; + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.detail-row { + display: flex; + gap: 16rpx; +} + +.detail-label { + font-size: 32rpx; + color: #364153; + width: 128rpx; + flex-shrink: 0; +} + +.detail-value { + font-size: 32rpx; + color: #4a5565; + flex: 1; +} + +/* 时间戳 */ +.time-stamp { + text-align: center; + padding: 32rpx 0; +} + +.time-text { + font-size: 24rpx; + color: #99a1af; +} + +/* 聊天消息 */ +.message-list { + display: flex; + flex-direction: column; + gap: 24rpx; +} + +.message-item { + display: flex; + gap: 16rpx; +} + +.message-avatar { + width: 80rpx; + height: 80rpx; + flex-shrink: 0; +} + +.avatar-placeholder { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +/* 消息头像图片样式 */ +.avatar-image-small { + width: 80rpx; + height: 80rpx; + border-radius: 50%; +} + +.avatar-text { + font-size: 32rpx; + font-weight: 700; + color: #fff; +} + +.message-bubble { + background: #fff; + border-radius: 0 20rpx 20rpx 20rpx; + padding: 24rpx; + box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1), 0 2rpx 4rpx rgba(0, 0, 0, 0.1); + max-width: 80%; +} + +.message-text { + font-size: 32rpx; + color: #1e2939; + line-height: 1.6; +} + +/* 服务中提示 */ +.busy-notice { + text-align: center; + padding: 32rpx 0; +} + +.busy-text { + font-size: 28rpx; + color: #6a7282; +} + +.remind-btn { + margin-top: 16rpx; +} + +.remind-text { + font-size: 32rpx; + color: #2b7fff; +} + +/* 底部操作按钮 */ +.action-buttons { + position: fixed; + bottom: calc(140rpx + env(safe-area-inset-bottom)); + left: 0; + right: 0; + display: flex; + gap: 16rpx; + padding: 0 32rpx 24rpx; + background: transparent; + z-index: 97; +} + +.action-btn { + flex: 1; + height: 100rpx; + border-radius: 100rpx; + display: flex; + align-items: center; + justify-content: center; + gap: 16rpx; + box-shadow: 0 8rpx 20rpx -6rpx rgba(0, 0, 0, 0.1), 0 20rpx 50rpx -10rpx rgba(0, 0, 0, 0.1); +} + +.action-btn.primary { + background: linear-gradient(90deg, #05df72 0%, #00bc7d 100%); +} + +.action-btn.secondary { + background: #fff; + border: 2rpx solid #e5e7eb; + box-shadow: 0 2rpx 4rpx -2rpx rgba(0, 0, 0, 0.1), 0 2rpx 6rpx 0 rgba(0, 0, 0, 0.1); +} + +.action-icon { + width: 40rpx; + height: 40rpx; +} + +.action-icon.white { + filter: brightness(0) invert(1); +} + +.action-emoji { + font-size: 32rpx; +} + +.action-text { + font-size: 32rpx; + color: #364153; + text-align: center; +} + +.action-text.white { + color: #fff; +} + +/* 电话图标样式 */ +.phone-icon-wrap { + width: 40rpx; + height: 40rpx; + position: relative; +} + +.phone-icon-svg { + width: 40rpx; + height: 40rpx; +} + +/* 面板打开时的透明遮罩层 - 点击关闭面板 */ +.panel-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: transparent; + z-index: 98; +} + +/* 底部输入区域容器 */ +.bottom-input-container { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: #fff; + z-index: 100; +} + +/* 面板打开时移除底部安全区域 */ +.bottom-input-container.panel-open .input-bar { + padding-bottom: 26rpx; +} + +/* 底部输入框 */ +.input-bar { + display: flex; + align-items: center; + gap: 24rpx; + padding: 26rpx 32rpx; + padding-bottom: calc(26rpx + env(safe-area-inset-bottom)); + background: #fff; + border-top: 2rpx solid #e5e7eb; +} + +.voice-btn { + width: 56rpx; + height: 56rpx; + border: 2rpx solid #99a1af; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.voice-icon-wrap { + display: flex; + align-items: center; + justify-content: center; +} + +.voice-bar { + width: 16rpx; + height: 24rpx; + background: #4a5565; + border-radius: 12rpx; +} + +.input-wrap { + flex: 1; + height: 88rpx; + background: #f3f4f6; + border-radius: 100rpx; + padding: 0 32rpx; + display: flex; + align-items: center; +} + +.message-input { + width: 100%; + font-size: 28rpx; + color: #101828; +} + +.message-input::placeholder { + color: #99a1af; +} + +.emoji-btn { + width: 56rpx; + height: 56rpx; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.emoji-icon { + width: 48rpx; + height: 48rpx; +} + +.add-btn { + width: 56rpx; + height: 56rpx; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +/* 加号图标 */ +.plus-icon { + width: 48rpx; + height: 48rpx; + position: relative; +} + +.plus-h { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 32rpx; + height: 4rpx; + background: #4a5565; + border-radius: 2rpx; +} + +.plus-v { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 4rpx; + height: 32rpx; + background: #4a5565; + border-radius: 2rpx; +} + +/* 底部安全区域 - 不再需要,已通过fixed定位处理 */ +.safe-area { + display: none; +} + + +/* 下单弹窗样式 */ +.order-modal-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; +} + +.order-modal { + position: fixed; + left: 0; + right: 0; + bottom: 0; + background: #fff; + border-radius: 32rpx 32rpx 0 0; + padding: 32rpx; + z-index: 1001; + animation: slideUp 0.3s ease; +} + +@keyframes slideUp { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 32rpx; +} + +.modal-title { + font-size: 36rpx; + font-weight: 700; + color: #101828; +} + +.modal-close { + font-size: 48rpx; + color: #99a1af; + line-height: 1; +} + +.section-label { + font-size: 28rpx; + color: #6a7282; + margin-bottom: 16rpx; + display: block; +} + +/* 服务类型选择 */ +.service-type-section { + margin-bottom: 32rpx; +} + +.service-type-options { + display: flex; + gap: 24rpx; +} + +.service-type-item { + flex: 1; + padding: 24rpx; + border: 2rpx solid #e5e7eb; + border-radius: 16rpx; + text-align: center; + transition: all 0.3s; +} + +.service-type-item.active { + border-color: #e91e63; + background: #fce7f3; +} + +.type-name { + display: block; + font-size: 28rpx; + color: #101828; + margin-bottom: 8rpx; +} + +.type-price { + font-size: 24rpx; + color: #e91e63; + font-weight: 600; +} + +/* 时长选择 */ +.duration-section { + margin-bottom: 32rpx; +} + +.duration-options { + display: flex; + gap: 24rpx; +} + +.duration-item { + flex: 1; + padding: 20rpx; + border: 2rpx solid #e5e7eb; + border-radius: 16rpx; + text-align: center; + transition: all 0.3s; +} + +.duration-item.active { + border-color: #e91e63; + background: #fce7f3; +} + +.duration-text { + font-size: 28rpx; + color: #101828; +} + +/* 价格汇总 */ +.price-summary { + display: flex; + justify-content: space-between; + align-items: center; + padding: 24rpx; + background: #f9fafb; + border-radius: 16rpx; + margin-bottom: 32rpx; +} + +.summary-label { + font-size: 28rpx; + color: #6a7282; +} + +.summary-price { + font-size: 40rpx; + font-weight: 700; + color: #e91e63; +} + +/* 确认按钮 */ +.confirm-btn { + width: 100%; + height: 96rpx; + background: linear-gradient(to right, #e91e63, #c2185b); + color: #fff; + font-size: 32rpx; + font-weight: 600; + border-radius: 48rpx; + border: none; +} + +.confirm-btn::after { + border: none; +} + + +/* ========== 人物介绍弹窗样式 - Figma设计 ========== */ +.profile-modal-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; +} + +.profile-modal { + position: fixed; + left: 0; + right: 0; + bottom: 0; + background: #fff; + border-radius: 64rpx 64rpx 0 0; + z-index: 1001; + max-height: 90vh; + display: flex; + flex-direction: column; + animation: slideUp 0.3s ease; + box-shadow: 0 -50rpx 100rpx -24rpx rgba(0, 0, 0, 0.25); +} + +.profile-modal.show { + transform: translateY(0); +} + +/* 弹窗头部 - 固定在顶部 */ +.profile-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 48rpx; + height: 154rpx; + background: rgba(255, 255, 255, 0.95); + border-bottom: 2rpx solid #f3f4f6; + border-radius: 64rpx 64rpx 0 0; + flex-shrink: 0; +} + +.profile-modal-title { + font-size: 40rpx; + font-weight: 900; + color: #101828; + line-height: 1.4; +} + +.profile-modal-close { + width: 72rpx; + height: 72rpx; + background: #f3f4f6; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.close-icon { + font-size: 40rpx; + color: #4a5565; + line-height: 1; +} + +/* 弹窗内容区 */ +.profile-modal-content { + flex: 1; + padding: 40rpx 48rpx; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 40rpx; +} + +/* 底部指示条 */ +.profile-modal-content::after { + content: ''; + display: block; + width: 224rpx; + height: 12rpx; + background: #d1d5dc; + border-radius: 100rpx; + margin: 40rpx auto 20rpx; + flex-shrink: 0; +} + +/* 基本信息区 - 卡片样式 */ +.profile-info-section { + display: flex; + gap: 40rpx; + padding: 32rpx; + background: #fff; + border: 2rpx solid #f3f4f6; + border-radius: 32rpx; +} + +.profile-avatar-large { + width: 180rpx; + height: 180rpx; + flex-shrink: 0; +} + +.avatar-placeholder-large { + width: 180rpx; + height: 180rpx; + border-radius: 32rpx; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8rpx 12rpx -8rpx rgba(0, 0, 0, 0.1), 0 20rpx 30rpx -6rpx rgba(0, 0, 0, 0.1); +} + +.avatar-text-large { + font-size: 64rpx; + font-weight: 700; + color: #fff; +} + +.avatar-img-large { + width: 100%; + height: 100%; + border-radius: 32rpx; +} + +.profile-basic-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 24rpx; + padding-top: 8rpx; +} + +.profile-name-large { + font-size: 48rpx; + font-weight: 900; + color: #101828; + line-height: 1.33; +} + +.profile-details-list { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.profile-detail-item { + font-size: 32rpx; + color: #364153; + line-height: 1.5; + display: flex; +} + +.profile-detail-item text:first-child, +.detail-label-text { + color: #6a7282; + font-weight: 700; + width: 120rpx; + flex-shrink: 0; +} + +.detail-value-text { + color: #364153; + font-weight: 400; +} + +/* 统计数据卡片 - 渐变背景 */ +.profile-stats-card { + background: linear-gradient(180deg, #faf8fb 0%, #f9f5fa 100%); + border: 2rpx solid rgba(232, 213, 240, 0.3); + border-radius: 32rpx; + padding: 42rpx; +} + +.stats-row { + display: flex; + justify-content: space-around; + align-items: center; +} + +.stats-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 12rpx; + flex: 1; +} + +.stats-value { + font-size: 60rpx; + font-weight: 900; + color: #101828; + line-height: 1.2; +} + +.stats-label { + font-size: 28rpx; + color: #4a5565; + line-height: 1.43; +} + +.stats-divider { + width: 2rpx; + height: 124rpx; + background: rgba(229, 231, 235, 0.6); +} + +/* 信息区块卡片 - 通用样式 */ +.profile-section-card { + background: #fff; + border: 2rpx solid #f3f4f6; + border-radius: 32rpx; + padding: 34rpx; +} + +/* 区块标题带装饰点 */ +.section-title { + font-size: 36rpx; + font-weight: 900; + color: #101828; + margin-bottom: 20rpx; + display: flex; + align-items: center; + gap: 28rpx; + line-height: 1.56; +} + +.section-title::before { + content: ''; + width: 12rpx; + height: 12rpx; + background: #b06ab3; + border-radius: 50%; + flex-shrink: 0; +} + +.section-content { + font-size: 32rpx; + color: #364153; + line-height: 1.625; + padding-left: 40rpx; +} + +/* 擅长领域标签 - 渐变背景 */ +.expertise-tags { + display: flex; + flex-wrap: wrap; + gap: 20rpx; + padding-left: 40rpx; +} + +.expertise-tag { + background: linear-gradient(180deg, #e8d5f0 0%, #f0e5f5 100%); + border: 2rpx solid rgba(216, 180, 230, 0.3); + border-radius: 100rpx; + padding: 14rpx 34rpx; +} + +.tag-text { + font-size: 32rpx; + font-weight: 700; + color: #8b4e9e; +} + +/* 聆听寄语 - 特殊渐变背景 */ +.profile-section-card.quote-card { + background: linear-gradient(180deg, #f9f5fa 0%, #faf8fb 100%); + border: 2rpx solid rgba(232, 213, 240, 0.5); + padding: 42rpx; +} + +.quote-card .section-content { + font-size: 32rpx; + color: #364153; + line-height: 1.8; + padding-left: 40rpx; +} + +/* ========== 评价弹窗样式 ========== */ +.review-modal-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; +} + +.review-modal { + position: fixed; + left: 0; + right: 0; + bottom: 0; + background: #f9fafb; + border-radius: 24rpx 24rpx 0 0; + z-index: 1001; + max-height: 85vh; + display: flex; + flex-direction: column; + animation: slideUp 0.3s ease; +} + +.review-modal.show { + transform: translateY(0); +} + +.review-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 32rpx; + background: #fff; + border-bottom: 2rpx solid #f3f4f6; + border-radius: 24rpx 24rpx 0 0; +} + +.review-modal-title { + font-size: 36rpx; + font-weight: 700; + color: #101828; +} + +.review-modal-close { + width: 48rpx; + height: 48rpx; + display: flex; + align-items: center; + justify-content: center; +} + +/* 评分汇总 */ +.review-summary { + display: flex; + justify-content: space-between; + align-items: center; + padding: 32rpx; + background: #fff; + border-bottom: 2rpx solid #f3f4f6; +} + +.summary-left { + display: flex; + align-items: center; + gap: 32rpx; +} + +.summary-avatar { + width: 128rpx; + height: 128rpx; +} + +.avatar-placeholder-small { + width: 128rpx; + height: 128rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.avatar-text-small { + font-size: 48rpx; + font-weight: 700; + color: #fff; +} + +.summary-score { + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.score-row { + display: flex; + align-items: baseline; + gap: 4rpx; +} + +.score-value { + font-size: 60rpx; + font-weight: 700; + color: #101828; +} + +.score-unit { + font-size: 28rpx; + color: #6a7282; +} + +.stars-row { + display: flex; + gap: 8rpx; +} + +.star-icon { + font-size: 28rpx; +} + +.review-count { + font-size: 24rpx; + color: #6a7282; +} + +.summary-right { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4rpx; +} + +.good-rate { + font-size: 48rpx; + font-weight: 700; + color: #ff6900; +} + +.good-rate-label { + font-size: 24rpx; + color: #6a7282; +} + +/* 评价列表 */ +.review-list { + flex: 1; + padding: 0 32rpx; + overflow-y: auto; +} + +.review-item { + background: #fff; + border-radius: 0; + padding: 32rpx; + margin-bottom: 2rpx; + border-bottom: 2rpx solid #f3f4f6; +} + +.review-item:first-child { + margin-top: 0; +} + +/* 用户信息行 */ +.review-user-row { + display: flex; + gap: 24rpx; + margin-bottom: 24rpx; +} + +.review-user-avatar { + width: 80rpx; + height: 80rpx; + flex-shrink: 0; +} + +.user-avatar-img { + width: 80rpx; + height: 80rpx; + border-radius: 50%; +} + +.user-avatar-placeholder { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + background: #e5e7eb; + display: flex; + align-items: center; + justify-content: center; +} + +.user-avatar-text { + font-size: 28rpx; + color: #6a7282; +} + +.review-user-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.user-name-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +.user-name { + font-size: 32rpx; + color: #101828; +} + +.review-date { + font-size: 24rpx; + color: #99a1af; +} + +.review-stars { + display: flex; + gap: 4rpx; +} + +.star-icon-small { + font-size: 24rpx; +} + +/* 评价内容 */ +.review-content-wrap { + margin-bottom: 24rpx; +} + +.review-content { + font-size: 28rpx; + color: #364153; + line-height: 1.625; +} + +.review-content.collapsed { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.expand-btn { + font-size: 28rpx; + color: #2b7fff; + margin-top: 8rpx; + display: inline-block; +} + +/* 评价标签 */ +.review-tags { + display: flex; + flex-wrap: wrap; + gap: 16rpx; + margin-bottom: 24rpx; +} + +.review-tag { + background: #fff7ed; + border-radius: 100rpx; + padding: 6rpx 20rpx; +} + +.review-tag-text { + font-size: 24rpx; + color: #f54900; +} + +/* 咨询师回复 */ +.counselor-reply { + background: #f9fafb; + border-radius: 20rpx; + padding: 24rpx; + margin-bottom: 24rpx; +} + +.reply-content { + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.reply-label { + font-size: 24rpx; + color: #6a7282; +} + +.reply-text { + font-size: 24rpx; + color: #4a5565; + line-height: 1.625; +} + +/* 点赞操作 */ +.review-actions { + display: flex; + justify-content: flex-end; +} + +.like-btn { + display: flex; + align-items: center; + gap: 8rpx; +} + +.like-btn.liked { + opacity: 0.6; +} + +.like-btn.liked .like-count { + color: #ff4d6d; +} + +.like-icon { + font-size: 32rpx; +} + +.like-count { + font-size: 24rpx; + color: #99a1af; +} + +/* 底部提示 */ +.review-bottom-tip { + text-align: center; + padding: 32rpx 0; +} + +.bottom-tip-text { + font-size: 28rpx; + color: #99a1af; +} + +.review-loading { + text-align: center; + padding: 32rpx 0; +} + +.loading-text { + font-size: 28rpx; + color: #99a1af; +} + +/* ========== 语音录音相关样式 ========== */ + +/* 语音按钮激活状态 */ +.voice-btn.active { + background: #e8d5f0; + border-color: #b06ab3; +} + +.voice-icon-img { + width: 32rpx; + height: 32rpx; +} + +/* 按住说话按钮 */ +.voice-record-btn { + width: 100%; + height: 88rpx; + background: #f3f4f6; + border-radius: 100rpx; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; +} + +.voice-record-btn.recording { + background: #e8d5f0; +} + +.voice-record-text { + font-size: 32rpx; + color: #4a5565; + font-weight: 500; +} + +.voice-record-btn.recording .voice-record-text { + color: #b06ab3; +} + +/* 录音中遮罩层 */ +.recording-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 2000; + display: flex; + align-items: center; + justify-content: center; +} + +/* 录音弹窗 */ +.recording-popup { + width: 360rpx; + padding: 48rpx 32rpx; + background: rgba(0, 0, 0, 0.8); + border-radius: 32rpx; + display: flex; + flex-direction: column; + align-items: center; + gap: 24rpx; +} + +.recording-popup.cancel { + background: rgba(220, 38, 38, 0.9); +} + +/* 录音图标 */ +.recording-icon-wrap { + width: 120rpx; + height: 120rpx; + background: rgba(255, 255, 255, 0.1); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +/* 录音波形动画 */ +.recording-wave { + display: flex; + align-items: center; + gap: 8rpx; + height: 60rpx; +} + +.wave-bar { + width: 8rpx; + height: 20rpx; + background: #fff; + border-radius: 4rpx; + animation: waveAnimation 0.5s ease-in-out infinite alternate; +} + +.wave-bar:nth-child(1) { animation-delay: 0s; } +.wave-bar:nth-child(2) { animation-delay: 0.1s; } +.wave-bar:nth-child(3) { animation-delay: 0.2s; } +.wave-bar:nth-child(4) { animation-delay: 0.1s; } +.wave-bar:nth-child(5) { animation-delay: 0s; } + +@keyframes waveAnimation { + from { + height: 20rpx; + } + to { + height: 50rpx; + } +} + +.recording-popup.cancel .wave-bar { + animation: none; + height: 20rpx; +} + +/* 录音提示文字 */ +.recording-tip { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.8); + text-align: center; +} + +/* 录音时长 */ +.recording-duration { + font-size: 48rpx; + font-weight: 700; + color: #fff; +} + + +/* ==================== 更多功能面板样式 ==================== */ +.more-panel { + background: #F5F5F5; + border-top: 2rpx solid #E5E7EB; +} + +.more-panel-content { + padding: 40rpx 32rpx 24rpx; +} + +.more-grid { + display: flex; + justify-content: space-between; +} + +.more-grid.second-row { + margin-top: 40rpx; +} + +.more-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 16rpx; + width: 25%; +} + +.more-icon-wrap { + width: 112rpx; + height: 112rpx; + background: #FFFFFF; + border-radius: 24rpx; + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.more-icon-img { + width: 56rpx; + height: 56rpx; +} + +.more-text { + font-size: 26rpx; + font-weight: 500; + color: #4A5565; +} + +.new-tag { + position: absolute; + top: -8rpx; + right: -8rpx; + background: #FF6B35; + color: #FFFFFF; + font-size: 18rpx; + font-weight: 700; + padding: 4rpx 10rpx; + border-radius: 8rpx; + line-height: 1; +} + +/* 相册图标 */ +.album-icon { + width: 48rpx; + height: 40rpx; + position: relative; +} + +.album-frame { + width: 100%; + height: 100%; + border: 4rpx solid #9CA3AF; + border-radius: 6rpx; + position: relative; +} + +.album-sun { + position: absolute; + top: 8rpx; + right: 8rpx; + width: 12rpx; + height: 12rpx; + background: #FBBF24; + border-radius: 50%; +} + +.album-mountain { + position: absolute; + bottom: 4rpx; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 12rpx solid transparent; + border-right: 12rpx solid transparent; + border-bottom: 16rpx solid #10B981; +} + +/* 常用语图标 */ +.quick-reply-icon { + width: 48rpx; + height: 40rpx; + position: relative; +} + +.reply-bubble1 { + position: absolute; + top: 0; + left: 0; + width: 32rpx; + height: 24rpx; + background: #E5E7EB; + border-radius: 12rpx 12rpx 12rpx 4rpx; +} + +.reply-bubble2 { + position: absolute; + bottom: 0; + right: 0; + width: 32rpx; + height: 24rpx; + background: #914584; + border-radius: 12rpx 12rpx 4rpx 12rpx; +} + +/* 红包图标 */ +.red-packet-icon { + width: 40rpx; + height: 52rpx; + position: relative; +} + +.packet-body { + width: 100%; + height: 100%; + background: #EF4444; + border-radius: 8rpx; +} + +.packet-top { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 20rpx; + background: #DC2626; + border-radius: 8rpx 8rpx 0 0; +} + +.packet-circle { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 16rpx; + height: 16rpx; + background: #FBBF24; + border-radius: 50%; +} + +.more-panel-safe { + height: env(safe-area-inset-bottom); + background: #F5F5F5; +} + +/* +号按钮激活状态 */ +.add-btn.active { + background: #E5E7EB; + border-radius: 8rpx; +} + +/* 表情按钮激活状态 */ +.emoji-btn.active { + background: #E5E7EB; + border-radius: 8rpx; +} + +/* ==================== 表情面板样式 ==================== */ +.emoji-panel { + background: #FFFFFF; + border-top: 2rpx solid #F3F4F6; +} + +.emoji-scroll { + height: 480rpx; + padding: 24rpx; + box-sizing: border-box; +} + +.emoji-grid { + display: flex; + flex-wrap: wrap; +} + +.emoji-item { + width: 12.5%; + height: 88rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.emoji-text { + font-size: 56rpx; + line-height: 1; +} + +.emoji-item:active { + background: #F3F4F6; + border-radius: 16rpx; +} + +/* 表情面板底部操作 */ +.emoji-actions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 24rpx; + padding: 16rpx 32rpx; + border-top: 2rpx solid #F3F4F6; + padding-bottom: calc(16rpx + env(safe-area-inset-bottom)); + background: #FFFFFF; +} + +.emoji-delete { + width: 80rpx; + height: 72rpx; + background: #F3F4F6; + border-radius: 12rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.delete-icon { + width: 40rpx; + height: 40rpx; + transform: rotate(180deg); +} + +.emoji-send { + width: 120rpx; + height: 72rpx; + background: #E5E7EB; + border-radius: 12rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + color: #9CA3AF; +} + +.emoji-send.active { + background: #914584; + color: #FFFFFF; +} diff --git a/pages/custom/custom.js b/pages/custom/custom.js new file mode 100644 index 0000000..11fc013 --- /dev/null +++ b/pages/custom/custom.js @@ -0,0 +1,46 @@ +const api = require('../../utils/api') + +Page({ + data: { + info: null, + loading: true, + error: null + }, + + onLoad() { + this.loadCustomInfo() + }, + + onBack() { + wx.navigateBack() + }, + + async loadCustomInfo() { + this.setData({ loading: true, error: null }) + + try { + const res = await api.common.getBrandConfig() + console.log('[custom] 定制服务配置响应:', res) + + if (res.success && res.data) { + const data = res.data + const customService = data.custom_service || {} + + this.setData({ + info: { + title: '定制服务', + content: customService.value || '' + } + }) + console.log('[custom] 定制服务信息:', this.data.info) + } else { + this.setData({ error: res.error || '加载失败' }) + } + } catch (err) { + console.error('[custom] 加载失败:', err) + this.setData({ error: err.message || '加载失败' }) + } finally { + this.setData({ loading: false }) + } + } +}) diff --git a/pages/custom/custom.json b/pages/custom/custom.json new file mode 100644 index 0000000..8835af0 --- /dev/null +++ b/pages/custom/custom.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/pages/custom/custom.wxml b/pages/custom/custom.wxml new file mode 100644 index 0000000..a9a4554 --- /dev/null +++ b/pages/custom/custom.wxml @@ -0,0 +1,38 @@ + + + + + + 返回 + + {{info.title}} + + + + + + + 加载中... + + + + + {{error}} + 点击重试 + + + + + + + + + + + + + + 暂无内容 + + + diff --git a/pages/custom/custom.wxss b/pages/custom/custom.wxss new file mode 100644 index 0000000..4cba098 --- /dev/null +++ b/pages/custom/custom.wxss @@ -0,0 +1,165 @@ +.page { + min-height: 100vh; + background-color: #f8f8f8; + display: flex; + flex-direction: column; +} + +.unified-header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 194rpx; + background: linear-gradient(135deg, #B06AB3 0%, #9B59B6 100%); + border-bottom: none; + display: flex; + align-items: flex-end; + justify-content: space-between; + padding: 0 32rpx 20rpx; + z-index: 1000; +} + +.unified-header-left { + display: flex; + align-items: center; + gap: 8rpx; + width: 160rpx; + height: 56rpx; +} + +.unified-back-icon { + width: 56rpx; + height: 56rpx; +} + +.unified-back-text { + font-size: 34rpx; + font-weight: bold; + color: #ffffff; +} + +.unified-header-title { + font-size: 40rpx; + font-weight: bold; + color: #ffffff; + flex: 1; + text-align: center; +} + +.unified-header-right { + width: 160rpx; + height: 56rpx; +} + +.content { + flex: 1; +} + +.loading-tip, .error-tip, .empty-tip { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: 200rpx; +} + +.loading-tip text, .empty-tip text, .error-tip text { + font-size: 28rpx; + color: #999; +} + +.error-tip text:first-child { + color: #ff6b6b; + margin-bottom: 20rpx; +} + +.retry-btn { + background: linear-gradient(135deg, #B06AB3 0%, #9B59B6 100%); + color: #fff !important; + padding: 16rpx 48rpx; + border-radius: 36rpx; + font-size: 28rpx !important; +} + +.empty-tip image { + width: 240rpx; + height: 240rpx; + margin-bottom: 20rpx; +} + +.custom-content { + padding: 30rpx; +} + +.intro-section { + background: #fff; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 30rpx; +} + +.intro-section rich-text { + font-size: 28rpx; + color: #666; + line-height: 1.8; +} + +.section-title { + font-size: 32rpx; + font-weight: bold; + color: #333; + margin-bottom: 20rpx; + padding-bottom: 16rpx; + border-bottom: 2rpx solid #f0f0f0; +} + +.services-section { + background: #fff; + border-radius: 20rpx; + padding: 30rpx; +} + +.services-list { + display: flex; + flex-direction: column; + gap: 16rpx; + margin-top: 20rpx; +} + +.service-item { + display: flex; + align-items: center; + padding: 24rpx; + background: #f8f8f8; + border-radius: 16rpx; +} + +.service-icon-box { + width: 80rpx; + height: 80rpx; + background: linear-gradient(135deg, #B06AB3 0%, #9B59B6 100%); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 20rpx; +} + +.service-info { + flex: 1; +} + +.service-name { + display: block; + font-size: 30rpx; + font-weight: bold; + color: #333; + margin-bottom: 8rpx; +} + +.service-desc { + display: block; + font-size: 24rpx; + color: #666; +} diff --git a/pages/customer-management/customer-management.js b/pages/customer-management/customer-management.js new file mode 100644 index 0000000..c573220 --- /dev/null +++ b/pages/customer-management/customer-management.js @@ -0,0 +1,233 @@ +// 客户管理页面 - 陪聊师端 +// 对接后端API + +const api = require('../../utils/api') +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + navHeight: 92, + loading: false, + // 统计数据 + balanceInt: '0', + balanceDec: '00', + pendingAmount: '0.00', + totalSettled: '0.00', + newCustomers: 0, + promotionOrders: 0, + // 搜索 + searchQuery: '', + // 客户列表 + customerList: [], + page: 1, + hasMore: true + }, + + onLoad() { + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const navHeight = statusBarHeight + 48 + + this.setData({ + statusBarHeight, + navHeight + }) + + this.loadData() + }, + + /** + * 下拉刷新 + */ + onPullDownRefresh() { + this.loadData().then(() => { + wx.stopPullDownRefresh() + }) + }, + + /** + * 上拉加载更多 + */ + onReachBottom() { + if (this.data.hasMore && !this.data.loading) { + this.loadMoreCustomers() + } + }, + + /** + * 加载数据 + */ + async loadData() { + await Promise.all([ + this.loadStats(), + this.loadCustomers() + ]) + }, + + /** + * 加载统计数据 + */ + async loadStats() { + try { + const res = await api.companion.getOrderStats() + + if (res.success && res.data) { + const balance = res.data.balance || 0 + const balanceStr = balance.toFixed(2) + const [balanceInt, balanceDec] = balanceStr.split('.') + + this.setData({ + balanceInt: parseInt(balanceInt).toLocaleString(), + balanceDec: balanceDec || '00', + pendingAmount: (res.data.pending_amount || 0).toFixed(2), + totalSettled: (res.data.total_settled || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }), + newCustomers: res.data.new_customers || 0, + promotionOrders: res.data.promotion_orders || 0 + }) + } + } catch (err) { + console.log('获取统计数据失败', err) + } + }, + + /** + * 加载客户列表 + */ + async loadCustomers() { + this.setData({ loading: true, page: 1 }) + + try { + const params = { page: 1, pageSize: 20 } + + if (this.data.searchQuery.trim()) { + params.keyword = this.data.searchQuery.trim() + } + + const res = await api.companion.getCustomers(params) + + if (res.success && res.data) { + const customers = (res.data.list || res.data || []).map(c => this.transformCustomer(c)) + + this.setData({ + customerList: customers, + hasMore: customers.length >= 20, + loading: false + }) + } else { + this.setData({ customerList: [], loading: false }) + } + } catch (err) { + console.error('加载客户列表失败', err) + this.setData({ loading: false }) + } + }, + + /** + * 加载更多客户 + */ + async loadMoreCustomers() { + const nextPage = this.data.page + 1 + this.setData({ loading: true }) + + try { + const params = { page: nextPage, pageSize: 20 } + + if (this.data.searchQuery.trim()) { + params.keyword = this.data.searchQuery.trim() + } + + const res = await api.companion.getCustomers(params) + + if (res.success && res.data) { + const newCustomers = (res.data.list || res.data || []).map(c => this.transformCustomer(c)) + + this.setData({ + customerList: [...this.data.customerList, ...newCustomers], + page: nextPage, + hasMore: newCustomers.length >= 20, + loading: false + }) + } else { + this.setData({ hasMore: false, loading: false }) + } + } catch (err) { + console.error('加载更多客户失败', err) + this.setData({ loading: false }) + } + }, + + /** + * 转换客户数据格式 + */ + transformCustomer(customer) { + return { + id: customer.id || customer.user_id, + displayId: `ID${String(customer.id || customer.user_id).slice(-4)}`, + nickname: customer.nickname || customer.name, + avatar: customer.avatar, + time: this.formatTime(customer.last_contact_at || customer.created_at), + status: customer.status || 'success', + orderCount: customer.order_count || 0, + totalAmount: customer.total_amount || 0 + } + }, + + /** + * 格式化时间 + */ + formatTime(dateStr) { + if (!dateStr) return '' + const date = new Date(dateStr) + const hour = String(date.getHours()).padStart(2, '0') + const minute = String(date.getMinutes()).padStart(2, '0') + return `${hour}:${minute}` + }, + + onBack() { + wx.navigateBack() + }, + + onSearchInput(e) { + this.setData({ searchQuery: e.detail.value }) + }, + + /** + * 搜索客户 + */ + onSearch() { + this.loadCustomers() + }, + + /** + * 点击客户 + */ + onCustomerTap(e) { + const customer = e.currentTarget.dataset.customer || {} + const id = e.currentTarget.dataset.id || customer.id + + wx.showActionSheet({ + itemList: ['查看详情', '发起聊天', '查看订单'], + success: (res) => { + if (res.tapIndex === 0) { + // 查看详情 + wx.showModal({ + title: '客户详情', + content: `客户ID: ${customer.displayId || id}\n订单数: ${customer.orderCount || 0}\n消费金额: ¥${customer.totalAmount || 0}`, + showCancel: false + }) + } else if (res.tapIndex === 1) { + // 发起聊天 + wx.navigateTo({ + url: `/pages/companion-chat/companion-chat?customerId=${id}` + }) + } else if (res.tapIndex === 2) { + // 查看订单 + wx.navigateTo({ + url: `/pages/orders/orders?customerId=${id}` + }) + } + } + }) + } +}) diff --git a/pages/customer-management/customer-management.json b/pages/customer-management/customer-management.json new file mode 100644 index 0000000..e90e996 --- /dev/null +++ b/pages/customer-management/customer-management.json @@ -0,0 +1,4 @@ +{ + "usingComponents": {}, + "navigationStyle": "custom" +} diff --git a/pages/customer-management/customer-management.wxml b/pages/customer-management/customer-management.wxml new file mode 100644 index 0000000..803ef9b --- /dev/null +++ b/pages/customer-management/customer-management.wxml @@ -0,0 +1,132 @@ + + + + + + + + + 返回 + + + + + + + + + + + + + + + + 快捷查询 + + + + + + + + + + + + 新增客户 + + + + + + {{newCustomers}} + + +5% + + + + + + + + + 推广订单 + + + + + + {{promotionOrders}} + + 66%转化 + + + + + + + + + 今日列表 + + + 实时更新 + + + + + + + {{item.id}} + {{item.time}} + + + + {{item.status === 'success' ? '注册成功' : '待确认'}} + + + + + + + + + + diff --git a/pages/customer-management/customer-management.wxss b/pages/customer-management/customer-management.wxss new file mode 100644 index 0000000..cb66d65 --- /dev/null +++ b/pages/customer-management/customer-management.wxss @@ -0,0 +1,508 @@ +/* 客户管理页面样式 */ +page { + background: #f5f2fd; +} + +.page-container { + min-height: 100vh; + background: #f5f2fd; + width: 100%; + overflow-x: hidden; + box-sizing: border-box; +} + +/* 隐藏滚动条 */ +.content-scroll::-webkit-scrollbar { + display: none; + width: 0; + height: 0; +} + +.content-scroll { + -ms-overflow-style: none; + scrollbar-width: none; +} + +/* 顶部导航 */ +.header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; +} + +.nav-bar { + display: flex; + align-items: center; + height: 48px; + padding: 0 16px; +} + +.back-btn { + display: flex; + align-items: center; + gap: 4px; + padding: 10px 8px; + border-radius: 50px; +} + +.back-icon { + width: 28px; + height: 28px; +} + +.back-text { + font-size: 18px; + font-weight: 700; + color: #101828; +} + +/* 内容区域 */ +.content-scroll { + min-height: 100vh; + padding: 0 16px; + box-sizing: border-box; + width: 100%; +} + +/* 账户卡片 */ +.account-card { + position: relative; + background: linear-gradient(180deg, #b06ab3 0%, #d489be 100%); + border-radius: 24px; + padding: 24px; + margin-bottom: 24px; + overflow: hidden; + box-shadow: 0 8px 24px rgba(176, 106, 179, 0.3); + box-sizing: border-box; + width: 100%; +} + +.account-bg { + position: absolute; + top: -48px; + right: -48px; + width: 192px; + height: 192px; + background: rgba(255, 255, 255, 0.1); + border-radius: 50%; + filter: blur(40px); +} + +.account-content { + position: relative; + z-index: 1; +} + +.account-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 24px; +} + +.account-icon-wrap { + width: 30px; + height: 30px; + background: rgba(255, 255, 255, 0.2); + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; +} + +.account-icon { + width: 18px; + height: 18px; +} + +.account-title { + font-size: 16px; + font-weight: 700; + color: #fff; + opacity: 0.95; +} + +.balance-section { + margin-bottom: 24px; +} + +.balance-label { + font-size: 14px; + color: rgba(255, 255, 255, 0.8); + margin-bottom: 6px; + display: block; +} + +.balance-value { + display: flex; + align-items: baseline; +} + +.balance-int { + font-size: 48px; + font-weight: 900; + color: #fff; + line-height: 1; +} + +.balance-dec { + font-size: 30px; + font-weight: 700; + color: rgba(255, 255, 255, 0.9); +} + +.stats-box { + background: rgba(0, 0, 0, 0.1); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 16px; + overflow: hidden; +} + +.stat-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; +} + +.stat-left { + display: flex; + align-items: center; + gap: 8px; +} + +.stat-icon { + width: 16px; + height: 16px; + opacity: 0.9; +} + +.stat-label { + font-size: 14px; + font-weight: 700; + color: rgba(255, 255, 255, 0.95); +} + +.stat-value { + font-size: 20px; + font-weight: 700; + color: #fff; +} + +.stat-divider { + height: 1px; + background: rgba(255, 255, 255, 0.1); + margin: 0 16px; +} + +/* 快捷查询 */ +.search-card { + background: #fff; + border-radius: 24px; + padding: 20px; + margin-bottom: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + box-sizing: border-box; + width: 100%; +} + +.search-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 20px; +} + +.search-icon-wrap { + width: 34px; + height: 34px; + background: #fdf4f9; + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; +} + +.search-icon { + width: 18px; + height: 18px; +} + +.search-title { + font-size: 16px; + font-weight: 700; + color: #101828; +} + +.search-input-wrap { + width: 100%; + box-sizing: border-box; +} + +.search-input { + width: 100%; + height: 48px; + background: #f9fafb; + border-radius: 16px; + padding: 0 16px; + font-size: 14px; + box-sizing: border-box; +} + +.search-input::placeholder { + color: #99a1af; +} + +.search-btn { + width: 100%; + margin-top: 12px; + background: #b06ab3; + color: #fff; + font-size: 14px; + font-weight: 700; + padding: 12px 0; + border-radius: 14px; + border: none; + box-shadow: 0 4px 8px rgba(176, 106, 179, 0.2); +} + +.search-btn::after { + border: none; +} + +/* 统计卡片 */ +.stats-cards { + display: flex; + gap: 12px; + margin-bottom: 16px; + width: 100%; + box-sizing: border-box; +} + +.stat-card { + flex: 1; + min-width: 0; + background: #fff; + border-radius: 16px; + padding: 16px; + position: relative; + overflow: hidden; + border: 1px solid #f3f4f6; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + box-sizing: border-box; +} + +.stat-card.active { + border: 1.1px solid #b06ab3; + box-shadow: 0 0 0 1px rgba(176, 106, 179, 0.2), 0 4px 6px -1px rgba(176, 106, 179, 0.1); +} + +.stat-card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.stat-card-title { + font-size: 14px; + font-weight: 700; + color: #6a7282; +} + +.stat-card.active .stat-card-title { + color: #b06ab3; +} + +.stat-card-icon-wrap { + width: 30px; + height: 30px; + background: #fdf4f9; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; +} + +.stat-card-icon-wrap.inactive { + background: #f3f4f6; +} + +.stat-card-icon { + width: 18px; + height: 18px; +} + +.stat-card-body { + display: flex; + align-items: flex-end; + gap: 8px; +} + +.stat-card-value { + font-size: 30px; + font-weight: 900; + color: #101828; + line-height: 1; +} + +.stat-card-value.inactive { + color: #99a1af; +} + +.stat-badge { + display: flex; + align-items: center; + gap: 2px; + padding: 2px 6px; + border-radius: 8px; + font-size: 12px; + font-weight: 700; + margin-bottom: 4px; +} + +.stat-badge.success { + background: #f0fdf4; + color: #00a63e; +} + +.stat-badge.neutral { + background: #f3f4f6; + color: #99a1af; +} + +.badge-icon { + width: 10px; + height: 10px; +} + +.stat-card-indicator { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 4px; + background: #b06ab3; +} + +/* 今日列表 */ +.list-card { + background: #fff; + border-radius: 24px; + padding: 24px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + box-sizing: border-box; + width: 100%; +} + +.list-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; + padding: 0 4px; +} + +.list-title { + font-size: 18px; + font-weight: 700; + color: #101828; +} + +.live-badge { + display: flex; + align-items: center; + gap: 8px; + background: #f0fdf4; + padding: 4px 10px; + border-radius: 10px; +} + +.live-dot { + width: 6px; + height: 6px; + background: #00c950; + border-radius: 50%; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.live-text { + font-size: 12px; + font-weight: 700; + color: #101828; +} + +.customer-list { + border-top: 1px solid #f9fafb; + padding-top: 8px; +} + +.customer-item { + display: flex; + align-items: center; + justify-content: space-between; + height: 57px; + padding: 0 8px; + border-bottom: 1px solid #f9fafb; +} + +.customer-item:last-child { + border-bottom: none; +} + +.customer-left { + display: flex; + align-items: center; + gap: 12px; +} + +.customer-id { + font-size: 16px; + font-weight: 700; + color: #101828; +} + +.customer-time { + font-size: 14px; + color: #99a1af; +} + +.customer-right { + display: flex; + align-items: center; + gap: 8px; +} + +.status-badge { + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 700; +} + +.status-badge.success { + background: #f0fdf4; + color: #00a63e; +} + +.status-badge.pending { + background: #fef3c7; + color: #d97706; +} + +.chevron-icon { + width: 16px; + height: 16px; + opacity: 0.5; +} + +.bottom-space { + height: 60px; +} diff --git a/pages/edit-profile/edit-profile.js b/pages/edit-profile/edit-profile.js new file mode 100644 index 0000000..825c1a5 --- /dev/null +++ b/pages/edit-profile/edit-profile.js @@ -0,0 +1,323 @@ +const { request, getBaseUrl } = require('../../utils_new/request'); +const util = require('../../utils/util'); + +Page({ + data: { + statusBarHeight: 20, + navBarHeight: 44, + totalNavHeight: 64, + defaultAvatar: '/images/default-avatar.svg', + loading: true, + saving: false, + uploading: false, + genderRange: ['男', '女'], + ageRanges: ['18-24岁', '25-35岁', '36-45岁', '46-55岁', '56岁以上'], + isProfileCompleted: false, + originalAvatar: '', + avatarUploadTemp: '', + form: { + nickname: '', + avatar: '', + gender: 0, // 1: 男, 2: 女 + age_range: '', + region: '', + phone: '' // 增加手机号显示 + } + }, + onLoad() { + const sys = wx.getSystemInfoSync(); + const menu = wx.getMenuButtonBoundingClientRect(); + const statusBarHeight = sys.statusBarHeight || 20; + const navBarHeight = menu.height + (menu.top - statusBarHeight) * 2; + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight: statusBarHeight + navBarHeight + }); + this.load(); + }, + onBack() { + wx.navigateBack({ delta: 1 }); + }, + async load() { + this.setData({ loading: true }); + try { + let profileData = { + nickname: '', + avatar: '', + gender: 0, + age_range: '', + region: '', + phone: '' + }; + let isProfileCompleted = false; + + try { + const res = await request({ url: '/api/users/profile', method: 'GET' }); + if (res.data && res.data.code === 0) { + const data = res.data.data; + profileData = { + nickname: data.nickname || '', + avatar: util.getFullImageUrl(data.avatar) || '', + gender: Number(data.gender || 0), + age_range: data.ageRange || data.age_range || '', + region: data.region || '', + phone: data.phone || '' + }; + isProfileCompleted = !!data.profile_completed; + } + } catch (err) { + console.log('API load failed'); + } + + this.setData({ + form: profileData, + isProfileCompleted, + originalAvatar: profileData.avatar || '' + }); + } finally { + this.setData({ loading: false }); + } + }, + + // 格式化图片地址,处理相对路径 + formatImageUrl(url) { + return util.getFullImageUrl(url); + }, + + // 将完整URL转回相对路径 + getRelativeUrl(url) { + if (!url) return ''; + if (url.startsWith('wxfile://') || url.startsWith('data:')) return ''; + const baseUrl = getBaseUrl(); + if (url.startsWith(baseUrl)) { + return url.replace(baseUrl, ''); + } + try { + if (url.startsWith('http://') || url.startsWith('https://')) { + const parsed = new URL(url); + const path = `${parsed.pathname}${parsed.search || ''}`; + return path; + } + } catch (_) {} + return url; + }, + + onChooseAvatar(e) { + if (e.detail.errMsg && e.detail.errMsg.indexOf('fail') !== -1) { + console.error('头像选择失败:', e.detail.errMsg); + // 如果是开发者工具的 Bug,提示用户重启 + if (e.detail.errMsg.indexOf('not found') !== -1) { + wx.showModal({ + title: '上传提示', + content: '微信开发者工具临时文件异常,请尝试重新点击或重启开发者工具。', + showCancel: false + }); + } + return; + } + + const { avatarUrl } = e.detail; + if (!avatarUrl) return; + + // 微信头像临时路径先显示,再上传 + this.setData({ + 'form.avatar': avatarUrl, + avatarUploadTemp: avatarUrl + }); + this.uploadAvatar(avatarUrl); + }, + + onNicknameInput(e) { + this.setData({ + 'form.nickname': e.detail.value + }); + }, + + onNicknameBlur(e) { + this.setData({ + 'form.nickname': e.detail.value + }); + }, + + onGenderChange(e) { + const idx = Number(e.detail.value); + const gender = idx + 1; // 0 -> 1 (男), 1 -> 2 (女) + this.setData({ + 'form.gender': gender + }); + }, + + onAgeRange(e) { + const idx = Number(e.detail.value); + const age_range = this.data.ageRanges[idx] || ''; + this.setData({ + 'form.age_range': age_range + }); + }, + + onRegion(e) { + const regionArray = e.detail.value || []; + const region = regionArray.join(' '); + this.setData({ + 'form.region': region, + 'form.regionArray': regionArray + }); + }, + + uploadAvatar(filePath) { + this.setData({ uploading: true }); + const token = wx.getStorageSync('auth_token') || ''; + const baseUrl = getBaseUrl(); + + // 获取设备ID + const deviceId = wx.getStorageSync('deviceId') || wx.getStorageSync('user_id') || 'unknown'; + + wx.uploadFile({ + url: `${baseUrl}/api/upload`, + filePath, + name: 'file', + formData: { folder: 'avatars' }, + header: { + 'x-device-id': deviceId, + ...(token ? { Authorization: `Bearer ${token}` } : {}) + }, + success: (res) => { + try { + const body = JSON.parse(res.data || '{}'); + if (body.code === 0 && body.data?.url) { + const fullUrl = this.formatImageUrl(body.data.url); + this.setData({ 'form.avatar': fullUrl, avatarUploadTemp: '' }); + wx.showToast({ title: '头像上传成功', icon: 'success' }); + } else { + wx.showToast({ title: body.message || '上传失败', icon: 'none' }); + this.setData({ + 'form.avatar': this.data.originalAvatar || '', + avatarUploadTemp: '' + }); + } + } catch (e) { + wx.showToast({ title: '解析返回失败', icon: 'none' }); + this.setData({ + 'form.avatar': this.data.originalAvatar || '', + avatarUploadTemp: '' + }); + } + }, + fail: (err) => { + console.error('upload failed', err); + wx.showToast({ title: '网络上传失败', icon: 'none' }); + this.setData({ + 'form.avatar': this.data.originalAvatar || '', + avatarUploadTemp: '' + }); + }, + complete: () => { + this.setData({ uploading: false }); + } + }); + }, + + onAvatarError() { + this.setData({ + 'form.avatar': this.data.defaultAvatar + }); + }, + + async save() { + if (this.data.saving) return; + if (this.data.uploading) { + wx.showToast({ title: '头像上传中,请稍候', icon: 'none' }); + return; + } + const form = this.data.form; + + const avatar = String(form.avatar || '').trim(); + if (avatar.startsWith('wxfile://') || avatar.startsWith('data:')) { + wx.showToast({ title: '头像尚未上传成功,请重新上传', icon: 'none' }); + return; + } + + // 验证完整信息 + if (!form.avatar || form.avatar === this.data.defaultAvatar) { + wx.showToast({ title: '请设置头像', icon: 'none' }); + return; + } + if (!String(form.nickname || '').trim()) { + wx.showToast({ title: '请输入昵称', icon: 'none' }); + return; + } + if (form.gender !== 1 && form.gender !== 2) { + wx.showToast({ title: '请选择性别', icon: 'none' }); + return; + } + if (!form.age_range) { + wx.showToast({ title: '请选择年龄段', icon: 'none' }); + return; + } + if (!form.region) { + wx.showToast({ title: '请选择所在地区', icon: 'none' }); + return; + } + + this.setData({ saving: true }); + try { + const payload = { + nickname: String(form.nickname || '').trim(), + avatar: this.getRelativeUrl(form.avatar), + gender: Number(form.gender), + age_range: form.age_range, + ageRange: form.age_range, + region: form.region, + regionArray: Array.isArray(form.regionArray) ? form.regionArray : undefined + }; + const res = await request({ + url: '/api/users/profile', + method: 'PUT', + data: payload + }); + const body = res.data || {}; + const ok = + (typeof body.code === 'number' && body.code === 0) || + (typeof body.success === 'boolean' && body.success === true); + if (!ok) throw new Error(body.message || body.error || '保存失败'); + + try { + const rewardRes = await request({ + url: '/api/love-points/profile-complete', + method: 'POST', + data: {} + }); + const rewardData = rewardRes.data || {}; + const innerData = rewardData.data || {}; + + if (rewardData.success && (innerData.earned > 0 || innerData.alreadyClaimed)) { + if (innerData.earned > 0) { + wx.showToast({ title: rewardData.message || `获得 ${innerData.earned} 爱心值`, icon: 'success' }); + } else { + wx.showToast({ title: '保存成功', icon: 'success' }); + } + this.setData({ isProfileCompleted: true }); + } else { + wx.showToast({ title: '保存成功', icon: 'success' }); + } + } catch (_) { + wx.showToast({ title: '保存成功', icon: 'success' }); + } + + // CRITICAL: Refresh global user info to ensure avatar consistency + const app = getApp(); + if (app && app.refreshUserInfo) { + app.refreshUserInfo(); + } + + this.setData({ originalAvatar: form.avatar || '' }); + + setTimeout(() => wx.navigateBack({ delta: 1 }), 500); + } catch (e) { + wx.showToast({ title: e.message || '保存失败', icon: 'none' }); + } finally { + this.setData({ saving: false }); + } + } +}); diff --git a/pages/edit-profile/edit-profile.json b/pages/edit-profile/edit-profile.json new file mode 100644 index 0000000..3153ca5 --- /dev/null +++ b/pages/edit-profile/edit-profile.json @@ -0,0 +1,5 @@ +{ + "usingComponents": { + "app-icon": "/components/icon/icon" + } +} diff --git a/pages/edit-profile/edit-profile.wxml b/pages/edit-profile/edit-profile.wxml new file mode 100644 index 0000000..681a62b --- /dev/null +++ b/pages/edit-profile/edit-profile.wxml @@ -0,0 +1,100 @@ + + + + + + + + + 个人信息 + + + + + + + + + + + + + 昵称 + + + + + + + 性别 + + + + {{form.gender === 1 ? '男' : (form.gender === 2 ? '女' : '去选择')}} + + + + + + + + 年龄段 + + + + {{form.age_range || '去选择'}} + + + + + + + + 地区 + + + + {{form.region || '去选择'}} + + + + + + + + + + + 手机号 + + {{form.phone || '未绑定'}} + + + + + + + + + + + 完善资料可获得 100 爱心值 + + + + diff --git a/pages/edit-profile/edit-profile.wxss b/pages/edit-profile/edit-profile.wxss new file mode 100644 index 0000000..e71231c --- /dev/null +++ b/pages/edit-profile/edit-profile.wxss @@ -0,0 +1,172 @@ +.page { + min-height: 100vh; + background-color: #F7F7F7; + padding-bottom: env(safe-area-inset-bottom); +} + +/* 导航栏样式 */ +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; + background-color: #F7F7F7; +} + +.nav-bar { + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.nav-back { + position: absolute; + left: 16px; + top: 50%; + transform: translateY(-50%); + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; +} + +.back-icon { + width: 24px; + height: 24px; +} + +.nav-title { + font-size: 17px; + font-weight: 600; + color: #000000; +} + +/* 容器样式 */ +.container { + padding-bottom: 40px; +} + +/* 单元格组样式 */ +.cell-group { + margin-top: 12px; + background-color: #FFFFFF; + border-top: 0.5px solid #EEEEEE; + border-bottom: 0.5px solid #EEEEEE; +} + +.cell { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + background-color: #FFFFFF; + line-height: 1.4; + font-size: 17px; + border: none; + border-radius: 0; + margin: 0; + width: 100% !important; + font-weight: normal; +} + +.cell::after { + display: none; +} + +.cell:not(:last-child) { + border-bottom: 0.5px solid #EEEEEE; + margin-left: 16px; + padding-left: 0; +} + +.cell-label { + color: #333333; + width: 80px; + text-align: left; +} + +.cell-value { + flex: 1; + display: flex; + align-items: center; + justify-content: flex-end; + color: #000000; + text-align: right; +} + +/* 头像单元格 */ +.avatar-cell { + padding: 12px 16px; +} + +.avatar { + width: 56px; + height: 56px; + border-radius: 8px; + margin-right: 8px; + background-color: #F0F0F0; +} + +/* 输入框样式 */ +.cell-input { + width: 100%; + height: 24px; + text-align: right; + color: #000000; +} + +.picker-value { + flex: 1; + margin-right: 4px; +} + +.picker-value.placeholder { + color: #CCCCCC; +} + +.phone-text { + color: #999999; +} + +.readonly-cell { + background-color: #FFFFFF; +} + +/* 按钮区域 */ +.btn-area { + margin-top: 40px; + padding: 0 16px; + display: flex; + flex-direction: column; + align-items: center; +} + +.save-btn { + width: 184px !important; + height: 48px; + line-height: 48px; + background-color: #07C160; + color: #FFFFFF; + font-size: 17px; + font-weight: 600; + border-radius: 8px; + border: none; + padding: 0; +} + +.save-btn[disabled] { + background-color: #F2F2F2; + color: #CCCCCC; +} + +.reward-tip { + margin-top: 16px; + display: flex; + align-items: center; + gap: 4px; + font-size: 14px; + color: #B06AB3; +} diff --git a/pages/eldercare-apply/eldercare-apply.js b/pages/eldercare-apply/eldercare-apply.js new file mode 100644 index 0000000..7049117 --- /dev/null +++ b/pages/eldercare-apply/eldercare-apply.js @@ -0,0 +1,162 @@ +// pages/eldercare-apply/eldercare-apply.js +// 智慧养老申请页面 +const api = require('../../utils/api') + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + showForm: true, + applyStatus: 'none', + statusTitle: '', + statusDesc: '', + isReapply: false, + agreed: false, + formData: { + realName: '', + city: '', + phone: '', + remarks: '' + }, + canSubmit: false + }, + + onLoad(options) { + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight, + isReapply: options.isReapply === 'true' + }) + + this.checkApplyStatus() + }, + + goBack() { + wx.navigateBack() + }, + + async checkApplyStatus() { + const token = wx.getStorageSync('auth_token') + if (!token) { + this.setData({ applyStatus: 'none' }) + return + } + + try { + const res = await api.request('/eldercare/apply') + if (res.success && res.data) { + const data = res.data + if (data.status === 'approved') { + this.setData({ + applyStatus: 'approved', + statusTitle: '申请已通过', + statusDesc: '恭喜您成为养老护理师!' + }) + } else if (data.status === 'pending') { + this.setData({ + applyStatus: 'pending', + statusTitle: '审核中', + statusDesc: '您的申请正在审核中,请耐心等待' + }) + } else if (data.status === 'rejected') { + this.setData({ + applyStatus: 'rejected', + statusTitle: '申请未通过', + statusDesc: data.rejectReason || '很抱歉,您的申请未通过审核' + }) + } + } + } catch (err) { + console.log('获取申请状态失败:', err) + this.setData({ applyStatus: 'none' }) + } + }, + + reapply() { + this.setData({ isReapply: true, applyStatus: 'none' }) + }, + + onInputChange(e) { + const field = e.currentTarget.dataset.field + this.setData({ [`formData.${field}`]: e.detail.value }) + this.checkCanSubmit() + }, + + toggleAgreement() { + this.setData({ agreed: !this.data.agreed }) + this.checkCanSubmit() + }, + + viewAgreement() { + wx.navigateTo({ url: '/pages/agreement/agreement?code=cooperation_service' }) + }, + + checkCanSubmit() { + const { formData, agreed } = this.data + + const canSubmit = + formData.realName && + formData.phone && + formData.phone.length === 11 && + agreed + + this.setData({ canSubmit }) + }, + + async submitApply() { + if (!this.data.canSubmit) return + + const { formData } = this.data + + if (!/^1[3-9]\d{9}$/.test(formData.phone)) { + wx.showToast({ title: '请输入正确的手机号', icon: 'none' }) + return + } + + wx.showLoading({ title: '提交中...' }) + try { + const res = await api.request('/eldercare/apply', { + method: 'POST', + data: { + realName: formData.realName, + city: formData.city, + phone: formData.phone, + remarks: formData.remarks + } + }) + + if (res.success || res.code === 0) { + wx.showToast({ title: '申请已提交', icon: 'success' }) + this.setData({ + applyStatus: 'pending', + statusTitle: '审核中', + statusDesc: '您的申请正在审核中,请耐心等待', + isReapply: false + }) + } else { + wx.showToast({ title: res.message || '提交失败', icon: 'none' }) + } + } catch (err) { + if (err.code === 404) { + wx.showModal({ + title: '提示', + content: '该服务申请即将开放,敬请期待!', + showCancel: false, + confirmColor: '#b06ab3' + }) + } else { + wx.showToast({ title: err.message || '提交失败', icon: 'none' }) + } + } finally { + wx.hideLoading() + } + } +}) diff --git a/pages/eldercare-apply/eldercare-apply.json b/pages/eldercare-apply/eldercare-apply.json new file mode 100644 index 0000000..a3e86ce --- /dev/null +++ b/pages/eldercare-apply/eldercare-apply.json @@ -0,0 +1,5 @@ +{ + "usingComponents": {}, + "navigationStyle": "custom", + "navigationBarTextStyle": "black" +} diff --git a/pages/eldercare-apply/eldercare-apply.wxml b/pages/eldercare-apply/eldercare-apply.wxml new file mode 100644 index 0000000..147a88b --- /dev/null +++ b/pages/eldercare-apply/eldercare-apply.wxml @@ -0,0 +1,191 @@ + + + + + + + + 返回 + + 智慧养老 + + + + + + + + + + + + + + + + + + 服务介绍 + + + 智慧养老服务为老年人提供专业的居家照护、健康管理、生活陪伴等服务,结合智能设备和专业护理,让老年人享受高品质的晚年生活。 + + + + + + + + + + 服务项目 + + + + 居家照护 + + + 健康监测 + + + 康复护理 + + + 生活陪伴 + + + 助餐服务 + + + 紧急救援 + + + + + + + + + + + 平台优势 + + + + + 专业护理团队,持证上岗 + + + + 智能设备辅助,实时监护 + + + + 24小时响应,紧急救援 + + + + 个性化服务,贴心关怀 + + + + + + + + 成为养老护理师,开启您的服务之旅 + + + + + + + + + + + + + {{statusTitle}} + {{statusDesc}} + + + + + + + + + 基本信息 + + + + + 姓名 + * + + + + + + + + + 所在城市 + (选填) + + + + + + + + + 手机号 + * + + + + + + + + + + + 备注信息 + + + + + + {{formData.remarks.length || 0}}/500 + + + + + + + + + + 我已阅读并同意 + 《合作入驻服务协议》 + + + + + + + + + + + diff --git a/pages/eldercare-apply/eldercare-apply.wxss b/pages/eldercare-apply/eldercare-apply.wxss new file mode 100644 index 0000000..d817662 --- /dev/null +++ b/pages/eldercare-apply/eldercare-apply.wxss @@ -0,0 +1,94 @@ +/* 智慧养老申请页面样式 */ +.page-container { + min-height: 100vh; + background: linear-gradient(180deg, #E8C3D4 0%, #F5E6EC 100%); +} + +.nav-header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + background: linear-gradient(135deg, #E8C3D4 0%, #D4A5C9 100%); +} + +.nav-content { + display: flex; + align-items: center; + justify-content: space-between; + height: 44px; + padding: 0 16px; +} + +.nav-back { display: flex; align-items: center; min-width: 60px; } +.back-icon { width: 20px; height: 20px; } +.back-text { font-size: 14px; color: #333; margin-left: 4px; } +.nav-title { font-size: 17px; font-weight: 600; color: #333; } +.nav-placeholder { min-width: 60px; } + +.content-scroll { height: 100vh; padding-bottom: env(safe-area-inset-bottom); } +.apply-form { padding: 16px; } +.status-card { background: #fff; border-radius: 16px; padding: 40px 24px; text-align: center; margin-bottom: 16px; } +.status-icon { width: 80px; height: 80px; margin: 0 auto 16px; } +.status-icon image { width: 100%; height: 100%; } +.status-title { display: block; font-size: 18px; font-weight: 600; color: #333; margin-bottom: 8px; } +.status-desc { font-size: 14px; color: #666; line-height: 1.6; } +.btn-secondary { margin-top: 24px; width: 160px; height: 44px; background: #fff; border: 1px solid #b06ab3; border-radius: 22px; color: #b06ab3; font-size: 15px; } + +.form-content { background: #fff; border-radius: 16px; padding: 24px 20px; } +.form-header { text-align: center; margin-bottom: 24px; } +.form-title { display: block; font-size: 20px; font-weight: 600; color: #333; margin-bottom: 8px; } +.form-subtitle { font-size: 14px; color: #999; } + +.form-section { margin-bottom: 24px; } +.section-header { display: flex; align-items: center; margin-bottom: 16px; } +.section-title { font-size: 16px; font-weight: 600; color: #333; } +.required { color: #ff4d4f; margin-left: 4px; } + +.avatar-upload-area { display: flex; justify-content: center; margin-bottom: 8px; } +.avatar-circle { width: 100px; height: 100px; border-radius: 50%; background: #f5f5f5; overflow: hidden; display: flex; align-items: center; justify-content: center; } +.avatar-image { width: 100%; height: 100%; } +.upload-placeholder { text-align: center; } +.camera-icon { width: 32px; height: 32px; margin-bottom: 4px; } +.upload-text { font-size: 12px; color: #999; } +.form-tip { font-size: 12px; color: #999; margin-top: 8px; } + +.form-item { margin-bottom: 16px; } +.item-label-row { display: flex; align-items: center; margin-bottom: 8px; } +.item-label { font-size: 14px; color: #333; } +.input-wrapper { background: #f8f8f8; border-radius: 8px; padding: 0 12px; } +.item-input { width: 100%; height: 44px; font-size: 14px; color: #333; } + +.gender-options { display: flex; gap: 12px; } +.gender-btn { flex: 1; height: 44px; background: #f8f8f8; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 14px; color: #666; } +.gender-btn.active { background: linear-gradient(135deg, #E8C3D4 0%, #D4A5C9 100%); color: #333; font-weight: 500; } + +.service-types { display: flex; flex-wrap: wrap; gap: 10px; } +.service-btn { padding: 8px 16px; background: #f8f8f8; border-radius: 20px; font-size: 13px; color: #666; } +.service-btn.active { background: linear-gradient(135deg, #E8C3D4 0%, #D4A5C9 100%); color: #333; } + +.level-options { display: flex; gap: 10px; } +.level-btn { flex: 1; padding: 10px 8px; background: #f8f8f8; border-radius: 8px; font-size: 13px; color: #666; text-align: center; } +.level-btn.active { background: linear-gradient(135deg, #E8C3D4 0%, #D4A5C9 100%); color: #333; font-weight: 500; } + +.cert-upload { width: 100%; height: 120px; background: #f8f8f8; border-radius: 8px; border: 1px dashed #ddd; display: flex; align-items: center; justify-content: center; overflow: hidden; } +.cert-image { width: 100%; height: 100%; } +.cert-placeholder { text-align: center; } +.upload-icon { width: 40px; height: 40px; margin-bottom: 8px; } + +.textarea-wrapper { background: #f8f8f8; border-radius: 8px; padding: 12px; } +.intro-textarea { width: 100%; height: 120px; font-size: 14px; color: #333; line-height: 1.6; } +.textarea-footer { display: flex; justify-content: flex-end; margin-top: 8px; } +.char-count { font-size: 12px; color: #999; } + +.agreement-row { display: flex; align-items: center; margin: 24px 0; } +.checkbox { width: 20px; height: 20px; border: 1px solid #ddd; border-radius: 4px; margin-right: 8px; display: flex; align-items: center; justify-content: center; } +.checkbox.checked { background: linear-gradient(135deg, #b06ab3 0%, #4568dc 100%); border-color: transparent; } +.check-icon { width: 14px; height: 14px; } +.normal-text { font-size: 13px; color: #666; } +.link-text { font-size: 13px; color: #b06ab3; } + +.submit-btn { width: 100%; height: 48px; background: linear-gradient(135deg, #b06ab3 0%, #4568dc 100%); border-radius: 24px; color: #fff; font-size: 16px; font-weight: 500; border: none; } +.submit-btn.disabled { opacity: 0.5; } +.bottom-placeholder { height: 40px; } diff --git a/pages/eldercare/eldercare.js b/pages/eldercare/eldercare.js new file mode 100644 index 0000000..7f5debf --- /dev/null +++ b/pages/eldercare/eldercare.js @@ -0,0 +1,46 @@ +const api = require('../../utils/api') + +Page({ + data: { + info: null, + loading: true, + error: null + }, + + onLoad() { + this.loadElderCareInfo() + }, + + onBack() { + wx.navigateBack() + }, + + async loadElderCareInfo() { + this.setData({ loading: true, error: null }) + + try { + const res = await api.common.getBrandConfig() + console.log('[eldercare] 智慧康养配置响应:', res) + + if (res.success && res.data) { + const data = res.data + const smartHealth = data.smart_health || {} + + this.setData({ + info: { + title: '智慧康养', + content: smartHealth.value || '' + } + }) + console.log('[eldercare] 智慧康养信息:', this.data.info) + } else { + this.setData({ error: res.error || '加载失败' }) + } + } catch (err) { + console.error('[eldercare] 加载失败:', err) + this.setData({ error: err.message || '加载失败' }) + } finally { + this.setData({ loading: false }) + } + } +}) diff --git a/pages/eldercare/eldercare.json b/pages/eldercare/eldercare.json new file mode 100644 index 0000000..8835af0 --- /dev/null +++ b/pages/eldercare/eldercare.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/pages/eldercare/eldercare.wxml b/pages/eldercare/eldercare.wxml new file mode 100644 index 0000000..aae22f4 --- /dev/null +++ b/pages/eldercare/eldercare.wxml @@ -0,0 +1,38 @@ + + + + + + 返回 + + {{info.title}} + + + + + + + 加载中... + + + + + {{error}} + 点击重试 + + + + + + + + + + + + + + 暂无内容 + + + diff --git a/pages/eldercare/eldercare.wxss b/pages/eldercare/eldercare.wxss new file mode 100644 index 0000000..a738a16 --- /dev/null +++ b/pages/eldercare/eldercare.wxss @@ -0,0 +1,210 @@ +.page { + min-height: 100vh; + background-color: #f8f8f8; + display: flex; + flex-direction: column; +} + +.unified-header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 194rpx; + background: linear-gradient(135deg, #B06AB3 0%, #9B59B6 100%); + border-bottom: none; + display: flex; + align-items: flex-end; + justify-content: space-between; + padding: 0 32rpx 20rpx; + z-index: 1000; +} + +.unified-header-left { + display: flex; + align-items: center; + gap: 8rpx; + width: 160rpx; + height: 56rpx; +} + +.unified-back-icon { + width: 56rpx; + height: 56rpx; +} + +.unified-back-text { + font-size: 34rpx; + font-weight: bold; + color: #ffffff; +} + +.unified-header-title { + font-size: 40rpx; + font-weight: bold; + color: #ffffff; + flex: 1; + text-align: center; +} + +.unified-header-right { + width: 160rpx; + height: 56rpx; +} + +.content { + flex: 1; +} + +.loading-tip, .error-tip, .empty-tip { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: 200rpx; +} + +.loading-tip text, .empty-tip text, .error-tip text { + font-size: 28rpx; + color: #999; +} + +.error-tip text:first-child { + color: #ff6b6b; + margin-bottom: 20rpx; +} + +.retry-btn { + background: linear-gradient(135deg, #B06AB3 0%, #9B59B6 100%); + color: #fff !important; + padding: 16rpx 48rpx; + border-radius: 36rpx; + font-size: 28rpx !important; +} + +.empty-tip image { + width: 240rpx; + height: 240rpx; + margin-bottom: 20rpx; +} + +.eldercare-content { + padding: 30rpx; +} + +.intro-section { + background: #fff; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 30rpx; +} + +.intro-section rich-text { + font-size: 28rpx; + color: #666; + line-height: 1.8; +} + +.section-title { + font-size: 32rpx; + font-weight: bold; + color: #333; + margin-bottom: 20rpx; + padding-bottom: 16rpx; + border-bottom: 2rpx solid #f0f0f0; +} + +.services-section { + background: #fff; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 30rpx; +} + +.services-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20rpx; + margin-top: 20rpx; +} + +.service-item { + display: flex; + flex-direction: column; + align-items: center; + padding: 30rpx 20rpx; + background: #f8f8f8; + border-radius: 16rpx; +} + +.service-icon { + width: 80rpx; + height: 80rpx; + background: linear-gradient(135deg, #B06AB3 0%, #9B59B6 100%); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 16rpx; +} + +.service-name { + font-size: 26rpx; + color: #333; + text-align: center; +} + +.articles-section { + background: #fff; + border-radius: 20rpx; + padding: 30rpx; +} + +.article-list { + display: flex; + flex-direction: column; + gap: 20rpx; + margin-top: 20rpx; +} + +.article-item { + display: flex; + padding: 20rpx; + background: #f8f8f8; + border-radius: 12rpx; +} + +.article-cover { + width: 180rpx; + height: 120rpx; + border-radius: 8rpx; + margin-right: 20rpx; + flex-shrink: 0; +} + +.article-info { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; +} + +.article-title { + font-size: 28rpx; + font-weight: bold; + color: #333; + margin-bottom: 8rpx; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + overflow: hidden; +} + +.article-desc { + font-size: 24rpx; + color: #666; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; +} diff --git a/pages/entertainment-apply/entertainment-apply.js b/pages/entertainment-apply/entertainment-apply.js new file mode 100644 index 0000000..4a3ab3d --- /dev/null +++ b/pages/entertainment-apply/entertainment-apply.js @@ -0,0 +1,162 @@ +// pages/entertainment-apply/entertainment-apply.js +// 休闲文娱申请页面 +const api = require('../../utils/api') + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + showForm: true, + applyStatus: 'none', + statusTitle: '', + statusDesc: '', + isReapply: false, + agreed: false, + formData: { + realName: '', + city: '', + phone: '', + remarks: '' + }, + canSubmit: false + }, + + onLoad(options) { + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight, + isReapply: options.isReapply === 'true' + }) + + this.checkApplyStatus() + }, + + goBack() { + wx.navigateBack() + }, + + async checkApplyStatus() { + const token = wx.getStorageSync('auth_token') + if (!token) { + this.setData({ applyStatus: 'none' }) + return + } + + try { + const res = await api.request('/entertainment/apply') + if (res.success && res.data) { + const data = res.data + if (data.status === 'approved') { + this.setData({ + applyStatus: 'approved', + statusTitle: '申请已通过', + statusDesc: '恭喜您成为休闲文娱服务师!' + }) + } else if (data.status === 'pending') { + this.setData({ + applyStatus: 'pending', + statusTitle: '审核中', + statusDesc: '您的申请正在审核中,请耐心等待' + }) + } else if (data.status === 'rejected') { + this.setData({ + applyStatus: 'rejected', + statusTitle: '申请未通过', + statusDesc: data.rejectReason || '很抱歉,您的申请未通过审核' + }) + } + } + } catch (err) { + console.log('获取申请状态失败:', err) + this.setData({ applyStatus: 'none' }) + } + }, + + reapply() { + this.setData({ isReapply: true, applyStatus: 'none' }) + }, + + onInputChange(e) { + const field = e.currentTarget.dataset.field + this.setData({ [`formData.${field}`]: e.detail.value }) + this.checkCanSubmit() + }, + + toggleAgreement() { + this.setData({ agreed: !this.data.agreed }) + this.checkCanSubmit() + }, + + viewAgreement() { + wx.navigateTo({ url: '/pages/agreement/agreement?code=cooperation_service' }) + }, + + checkCanSubmit() { + const { formData, agreed } = this.data + + const canSubmit = + formData.realName && + formData.phone && + formData.phone.length === 11 && + agreed + + this.setData({ canSubmit }) + }, + + async submitApply() { + if (!this.data.canSubmit) return + + const { formData } = this.data + + if (!/^1[3-9]\d{9}$/.test(formData.phone)) { + wx.showToast({ title: '请输入正确的手机号', icon: 'none' }) + return + } + + wx.showLoading({ title: '提交中...' }) + try { + const res = await api.request('/entertainment/apply', { + method: 'POST', + data: { + realName: formData.realName, + city: formData.city, + phone: formData.phone, + remarks: formData.remarks + } + }) + + if (res.success || res.code === 0) { + wx.showToast({ title: '申请已提交', icon: 'success' }) + this.setData({ + applyStatus: 'pending', + statusTitle: '审核中', + statusDesc: '您的申请正在审核中,请耐心等待', + isReapply: false + }) + } else { + wx.showToast({ title: res.message || '提交失败', icon: 'none' }) + } + } catch (err) { + if (err.code === 404) { + wx.showModal({ + title: '提示', + content: '该服务申请即将开放,敬请期待!', + showCancel: false, + confirmColor: '#b06ab3' + }) + } else { + wx.showToast({ title: err.message || '提交失败', icon: 'none' }) + } + } finally { + wx.hideLoading() + } + } +}) diff --git a/pages/entertainment-apply/entertainment-apply.json b/pages/entertainment-apply/entertainment-apply.json new file mode 100644 index 0000000..a3e86ce --- /dev/null +++ b/pages/entertainment-apply/entertainment-apply.json @@ -0,0 +1,5 @@ +{ + "usingComponents": {}, + "navigationStyle": "custom", + "navigationBarTextStyle": "black" +} diff --git a/pages/entertainment-apply/entertainment-apply.wxml b/pages/entertainment-apply/entertainment-apply.wxml new file mode 100644 index 0000000..bf9ec6f --- /dev/null +++ b/pages/entertainment-apply/entertainment-apply.wxml @@ -0,0 +1,103 @@ + + + + + + + + 返回 + + 休闲文娱 + + + + + + + + + + + + + + + + {{statusTitle}} + {{statusDesc}} + + + + + + + + + 基本信息 + + + + + 姓名 + * + + + + + + + + + 所在城市 + (选填) + + + + + + + + + 手机号 + * + + + + + + + + + + + 备注信息 + + + + + + {{formData.remarks.length || 0}}/500 + + + + + + + + + + 我已阅读并同意 + 《合作入驻服务协议》 + + + + + + + + + + + diff --git a/pages/entertainment-apply/entertainment-apply.wxss b/pages/entertainment-apply/entertainment-apply.wxss new file mode 100644 index 0000000..9b3292e --- /dev/null +++ b/pages/entertainment-apply/entertainment-apply.wxss @@ -0,0 +1,169 @@ +/* 休闲文娱申请页面样式 - 玫瑰紫版 v3.0 */ +.page-container { + min-height: 100vh; + background: linear-gradient(180deg, #E8C3D4 0%, #F5E6ED 100%); +} + +.nav-header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + background: rgba(248, 249, 252, 0.75); + backdrop-filter: blur(20rpx) saturate(180%); +} + +.nav-content { + display: flex; + align-items: center; + justify-content: space-between; + height: 44px; + padding: 0 16px; +} + +.nav-back { display: flex; align-items: center; min-width: 60px; } +.back-icon { width: 20px; height: 20px; } +.back-text { font-size: 14px; color: #111827; margin-left: 4px; } +.nav-title { font-size: 17px; font-weight: 600; color: #111827; } +.nav-placeholder { min-width: 60px; } + +.content-scroll { height: 100vh; padding-bottom: env(safe-area-inset-bottom); } +.intro-section { padding: 16px; } + +.banner-card { + height: 160px; + border-radius: 16px; + overflow: hidden; + margin-bottom: 16px; + background: linear-gradient(135deg, #914584 0%, #7A3A6F 100%); + box-shadow: 0 4rpx 16rpx rgba(145, 69, 132, 0.25); +} + +.banner-gradient { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.banner-title { font-size: 24px; font-weight: 600; color: #fff; margin-bottom: 8px; } +.banner-subtitle { font-size: 14px; color: rgba(255,255,255,0.9); } + +.info-card { + background: #fff; + border-radius: 16px; + padding: 20px; + margin-bottom: 16px; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06), + 0 2rpx 8rpx rgba(0, 0, 0, 0.04); + transition: transform 0.25s ease, box-shadow 0.25s ease; +} + +.card-header { display: flex; align-items: center; margin-bottom: 16px; } + +.card-icon { + width: 32px; + height: 32px; + background: linear-gradient(135deg, #914584 0%, #7A3A6F 100%); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 12px; + box-shadow: 0 2rpx 8rpx rgba(145, 69, 132, 0.2); +} + +.card-icon image { width: 20px; height: 20px; } +.card-title { font-size: 18px; font-weight: 600; color: #111827; } +.intro-text { font-size: 14px; color: #6B7280; line-height: 1.8; } + +.service-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } +.service-item { background: #f8f8f8; border-radius: 8px; padding: 12px 8px; text-align: center; transition: transform 0.2s ease; } +.service-item:active { transform: scale(0.95); } +.service-name { font-size: 13px; color: #111827; } + +.advantage-list { padding: 0; } +.advantage-item { display: flex; align-items: center; padding: 8px 0; } +.advantage-dot { width: 6px; height: 6px; background: linear-gradient(135deg, #914584 0%, #7A3A6F 100%); border-radius: 50%; margin-right: 12px; } +.advantage-text { font-size: 14px; color: #6B7280; } + +.apply-btn-area { padding: 24px 0; text-align: center; } +.apply-btn { width: 100%; height: 48px; background: linear-gradient(135deg, #914584 0%, #7A3A6F 100%); border-radius: 24px; color: #fff; font-size: 16px; font-weight: 500; border: none; margin-bottom: 12px; box-shadow: 0 4rpx 16rpx rgba(145, 69, 132, 0.35); transition: all 0.25s ease; } +.apply-btn:active { transform: scale(0.98); box-shadow: 0 2rpx 8rpx rgba(145, 69, 132, 0.4); } +.apply-tip { font-size: 13px; color: #9CA3AF; } + +.apply-form { padding: 16px; } +.status-card { background: #fff; border-radius: 16px; padding: 40px 24px; text-align: center; margin-bottom: 16px; } +.status-icon { width: 80px; height: 80px; margin: 0 auto 16px; } +.status-icon image { width: 100%; height: 100%; } +.status-title { display: block; font-size: 18px; font-weight: 600; color: #111827; margin-bottom: 8px; } +.status-desc { font-size: 14px; color: #6B7280; line-height: 1.6; } +.btn-secondary { margin-top: 24px; width: 160px; height: 44px; background: #fff; border: 1px solid #914584; border-radius: 22px; color: #914584; font-size: 15px; transition: all 0.2s ease; } +.btn-secondary:active { transform: scale(0.95); background: #F5E6ED; } + +.form-content { background: #fff; border-radius: 16px; padding: 24px 20px; box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06), + 0 2rpx 8rpx rgba(0, 0, 0, 0.04); } +.form-header { text-align: center; margin-bottom: 24px; } +.form-title { display: block; font-size: 20px; font-weight: 600; color: #111827; margin-bottom: 8px; } +.form-subtitle { font-size: 14px; color: #9CA3AF; } + +.form-section { margin-bottom: 24px; } +.section-header { display: flex; align-items: center; margin-bottom: 16px; } +.section-title { font-size: 16px; font-weight: 600; color: #111827; } +.required { color: #ff4d4f; margin-left: 4px; } + +.avatar-upload-area { display: flex; justify-content: center; margin-bottom: 8px; } +.avatar-circle { width: 100px; height: 100px; border-radius: 50%; background: linear-gradient(135deg, #E5E7EB 0%, #F3F4F6 100%); overflow: hidden; display: flex; align-items: center; justify-content: center; border: 2px solid #F1F5F9; } +.avatar-image { width: 100%; height: 100%; } +.upload-placeholder { text-align: center; } +.camera-icon { width: 32px; height: 32px; margin-bottom: 4px; } +.upload-text { font-size: 12px; color: #9CA3AF; } +.form-tip { font-size: 12px; color: #9CA3AF; margin-top: 8px; } + +.form-item { margin-bottom: 16px; } +.item-label-row { display: flex; align-items: center; margin-bottom: 8px; } +.item-label { font-size: 14px; color: #111827; } +.input-wrapper { background: #f8f8f8; border-radius: 8px; padding: 0 12px; transition: background 0.2s ease; } +.input-wrapper:focus-within { background: #F3F4F6; } +.item-input { width: 100%; height: 44px; font-size: 14px; color: #111827; } + +.gender-options { display: flex; gap: 12px; } +.gender-btn { flex: 1; height: 44px; background: #f8f8f8; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 14px; color: #6B7280; transition: all 0.2s ease; } +.gender-btn:active { transform: scale(0.95); } +.gender-btn.active { background: linear-gradient(135deg, #914584 0%, #7A3A6F 100%); color: #fff; font-weight: 500; box-shadow: 0 2rpx 8rpx rgba(145, 69, 132, 0.3); } + +.service-types { display: flex; flex-wrap: wrap; gap: 10px; } +.service-btn { padding: 8px 16px; background: #f8f8f8; border-radius: 20px; font-size: 13px; color: #6B7280; transition: all 0.2s ease; } +.service-btn:active { transform: scale(0.95); } +.service-btn.active { background: linear-gradient(135deg, #914584 0%, #7A3A6F 100%); color: #fff; box-shadow: 0 2rpx 8rpx rgba(145, 69, 132, 0.3); } + +.level-options { display: flex; gap: 10px; } +.level-btn { flex: 1; padding: 10px 8px; background: #f8f8f8; border-radius: 8px; font-size: 13px; color: #6B7280; text-align: center; transition: all 0.2s ease; } +.level-btn:active { transform: scale(0.95); } +.level-btn.active { background: linear-gradient(135deg, #914584 0%, #7A3A6F 100%); color: #fff; font-weight: 500; box-shadow: 0 2rpx 8rpx rgba(145, 69, 132, 0.3); } + +.cert-upload { width: 100%; height: 120px; background: linear-gradient(135deg, #E5E7EB 0%, #F3F4F6 100%); border-radius: 8px; border: 1px dashed #D1D5DB; display: flex; align-items: center; justify-content: center; overflow: hidden; transition: border-color 0.2s ease; } +.cert-upload:active { border-color: #914584; } +.cert-image { width: 100%; height: 100%; } +.cert-placeholder { text-align: center; } +.upload-icon { width: 40px; height: 40px; margin-bottom: 8px; } + +.textarea-wrapper { background: #f8f8f8; border-radius: 8px; padding: 12px; } +.intro-textarea { width: 100%; height: 120px; font-size: 14px; color: #111827; line-height: 1.6; } +.textarea-footer { display: flex; justify-content: flex-end; margin-top: 8px; } +.char-count { font-size: 12px; color: #9CA3AF; } + +.agreement-row { display: flex; align-items: center; margin: 24px 0; } +.checkbox { width: 20px; height: 20px; border: 1px solid #D1D5DB; border-radius: 4px; margin-right: 8px; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; } +.checkbox.checked { background: linear-gradient(135deg, #914584 0%, #7A3A6F 100%); border-color: transparent; box-shadow: 0 2rpx 8rpx rgba(145, 69, 132, 0.3); } +.check-icon { width: 14px; height: 14px; } +.normal-text { font-size: 13px; color: #6B7280; } +.link-text { font-size: 13px; color: #914584; } + +.submit-btn { width: 100%; height: 48px; background: linear-gradient(135deg, #914584 0%, #7A3A6F 100%); border-radius: 24px; color: #fff; font-size: 16px; font-weight: 500; border: none; box-shadow: 0 4rpx 16rpx rgba(145, 69, 132, 0.35); transition: all 0.25s ease; } +.submit-btn:active { transform: scale(0.98); box-shadow: 0 2rpx 8rpx rgba(145, 69, 132, 0.4); } +.submit-btn.disabled { opacity: 0.5; transform: none; } +.bottom-placeholder { height: 40px; } diff --git a/pages/entertainment/entertainment.js b/pages/entertainment/entertainment.js new file mode 100644 index 0000000..a4c666c --- /dev/null +++ b/pages/entertainment/entertainment.js @@ -0,0 +1,894 @@ +// pages/entertainment/entertainment.js - 休闲文娱页面 +// 根据Figma设计实现 + +const api = require('../../utils/api') +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + loading: false, + loadingMore: false, + + // 顶部轮播数据 - 从后台素材管理API加载 + bannerList: [], + swiperHeight: 400, + currentBannerIndex: 0, + + // 功能入口 - 使用正式环境图片URL + categoryList: [ + { + id: 1, + name: '兴趣搭子', + icon: '/images/icon-interest.png' + }, + { + id: 2, + name: '同城活动', + icon: '/images/icon-city.png' + }, + { + id: 3, + name: '户外郊游', + icon: '/images/icon-outdoor.png' + }, + { + id: 4, + name: '高端定制', + icon: '/images/icon-travel.png' + }, + { + id: 5, + name: '快乐学堂', + icon: '/images/icon-checkin.png' + }, + { + id: 6, + name: '单身聚会', + icon: '/images/icon-love.png' + } + ], + + // 滚动公告 + noticeList: [], + currentNoticeIndex: 0, + + // 活动标签 + activeTab: 'featured', // featured: 精选活动, free: 免费活动, vip: VIP活动, svip: SVIP活动 + + // 活动列表 + activityList: [], + + // 分页相关 + page: 1, + limit: 20, + hasMore: true, + total: 0, + + // 二维码引导弹窗 + showQrcodeModal: false, + qrcodeImageUrl: '', + + // 未读消息数 + totalUnread: 0, + + // 审核状态 + auditStatus: 0 + }, + + onLoad() { + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight + }) + + this.loadPageData() + this.loadNotices() + this.startNoticeScroll() + }, + + /** + * 加载页面数据 + */ + async loadPageData() { + // 并行加载Banner、功能入口和活动列表 + await Promise.all([ + this.loadBanners(), + this.loadEntries(), + this.loadActivityList() + ]) + }, + + /** + * 处理图片URL,如果是相对路径则拼接域名,并设置清晰度为85 + */ + processImageUrl(url) { + if (!url) return '' + let fullUrl = url + if (!url.startsWith('http://') && !url.startsWith('https://')) { + const baseUrl = 'https://ai-c.maimanji.com' + fullUrl = baseUrl + (url.startsWith('/') ? '' : '/') + url + } + + // 添加清晰度参数 q=85 + if (fullUrl.includes('?')) { + if (!fullUrl.includes('q=')) { + fullUrl += '&q=85' + } + } else { + fullUrl += '?q=85' + } + return fullUrl + }, + + /** + * 轮播图图片加载完成,自适应高度 + */ + onBannerLoad(e) { + if (this.data.swiperHeight !== 300) return; // 只计算一次 + const { width, height } = e.detail; + const sysInfo = wx.getSystemInfoSync(); + // 减去左右padding (32rpx * 2) + const swiperWidth = sysInfo.windowWidth - (32 * 2 / 750 * sysInfo.windowWidth); + const ratio = width / height; + const swiperHeight = swiperWidth / ratio; + const swiperHeightRpx = swiperHeight * (750 / sysInfo.windowWidth); + + this.setData({ + swiperHeight: swiperHeightRpx + }); + }, + + /** + * 加载功能入口图标 + * 从后台素材管理API加载 (group=entries) + */ + async loadEntries() { + try { + const res = await api.pageAssets.getAssets('entries') + console.log('功能入口 API响应:', res) + + if (res.success && res.data) { + const icons = res.data + const { categoryList } = this.data + + // 映射图标:搭子(id=1), 同城(id=2), 户外(id=3), 定制(id=4), 学堂(id=5), 传递(id=6) + const idMap = { + 1: 'entry_1', // 兴趣搭子 + 2: 'entry_2', // 同城活动 + 3: 'entry_3', // 户外郊游 + 4: 'entry_4', // 定制主题 + 5: 'entry_5', // 快乐学堂 + 6: 'entry_6' // 爱心传递 + } + + const updatedCategoryList = categoryList.map(item => { + const assetKey = idMap[item.id] + const iconUrl = icons[assetKey] + if (iconUrl) { + return { + ...item, + icon: this.processImageUrl(iconUrl) + } + } + return item + }) + + this.setData({ categoryList: updatedCategoryList }) + console.log('已更新娱乐页功能入口图标') + } + } catch (err) { + console.error('加载功能入口失败', err) + } + }, + + /** + * 加载娱乐页Banner + * 调用专用API:/api/page-assets/entertainment-banners + */ + async loadBanners() { + try { + const res = await api.pageAssets.getEntertainmentBanners() + + console.log('娱乐页Banner API响应:', res) + + if (res.success && res.data) { + // 处理相对路径,拼接完整URL + // 兼容不同可能的字段名:asset_url, url, imageUrl + const bannerList = res.data.map(item => { + const rawUrl = item.asset_url || item.url || item.imageUrl || item.image_url || '' + return { + id: item.id, + imageUrl: this.processImageUrl(rawUrl), + // 只保留标签,不显示标题和副标题 + tag: item.description || item.tag || '热门推荐', + title: '', + subtitle: '', + bgColor: item.bg_color || item.bgColor || 'linear-gradient(135deg, #E8D5F0 0%, #F5E6D3 100%)' + } + }).filter(item => item.imageUrl) // 只保留有图片的 + + if (bannerList.length > 0) { + this.setData({ bannerList }) + console.log(`加载了 ${bannerList.length} 个娱乐页Banner`) + } else { + console.log('娱乐页Banner数据为空或解析失败,使用默认配置') + this.setDefaultBanners() + } + } else { + console.log('娱乐页Banner API返回失败,使用默认配置') + this.setDefaultBanners() + } + } catch (err) { + console.error('加载娱乐页Banner失败', err) + this.setDefaultBanners() + } + }, + + /** + * 设置默认Banner(降级方案 - 使用CDN URL) + */ + setDefaultBanners() { + const cdnBase = 'https://ai-c.maimanji.com/images' + this.setData({ + bannerList: [ + { + id: 1, + imageUrl: `${cdnBase}/service-banner-1.png`, + tag: '热门', + title: '', + subtitle: '', + bgColor: 'linear-gradient(135deg, #E8D5F0 0%, #F5E6D3 100%)' + }, + { + id: 2, + imageUrl: `${cdnBase}/service-banner-2.png`, + tag: '活动', + title: '', + subtitle: '', + bgColor: 'linear-gradient(135deg, #D5E8F0 0%, #E6F5D3 100%)' + }, + { + id: 3, + imageUrl: `${cdnBase}/service-banner-3.png`, + tag: '推荐', + title: '', + subtitle: '', + bgColor: 'linear-gradient(135deg, #F0E8D5 0%, #F5D3E6 100%)' + } + ] + }) + console.log('使用默认娱乐页Banner配置') + }, + + /** + * 加载公告 + */ + async loadNotices() { + try { + const res = await api.common.getNotices() + console.log('[notice] 公告API响应:', res) + + if (res.success && res.data && res.data.length > 0) { + const noticeList = res.data.map(item => ({ + id: item.id, + content: item.content, + linkType: item.linkType || 'none', + linkValue: item.linkValue || '' + })) + this.setData({ noticeList }) + } + } catch (err) { + console.error('[notice] 加载公告失败', err) + } + }, + + /** + * 点击公告栏 + */ + onNoticeTap() { + wx.navigateTo({ + url: '/pages/notices/notices' + }) + }, + + onShow() { + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ selected: 1 }) + } + wx.hideTabBar({ animation: false }) + const app = getApp() + this.setData({ + auditStatus: app.globalData.auditStatus + }) + this.loadUnreadCount() + }, + + onUnload() { + if (this.noticeTimer) { + clearInterval(this.noticeTimer) + } + }, + + + /** + * 开始公告滚动 + */ + startNoticeScroll() { + this.noticeTimer = setInterval(() => { + const { noticeList, currentNoticeIndex } = this.data + const nextIndex = (currentNoticeIndex + 1) % noticeList.length + this.setData({ currentNoticeIndex: nextIndex }) + }, 3000) + }, + + /** + * 加载活动列表 - 根据activeTab加载不同的活动(支持分页) + */ + async loadActivityList(isLoadMore = false) { + console.log('========== 加载活动列表 ==========') + console.log('[6] activeTab:', this.data.activeTab) + console.log('[6.1] isLoadMore:', isLoadMore) + + if (isLoadMore) { + this.setData({ loadingMore: true }) + } else { + this.setData({ loading: true, page: 1, hasMore: true }) + } + + try { + const config = require('../../config/index') + const { activeTab, page, limit } = this.data + + console.log('[8] 请求URL:', `${config.API_BASE_URL}/entertainment/home`) + console.log('[9] 请求参数:', { type: activeTab, page, limit }) + + const res = await new Promise((resolve, reject) => { + wx.request({ + url: `${config.API_BASE_URL}/entertainment/home`, + method: 'GET', + data: { + type: activeTab, + page, + limit + }, + timeout: 10000, + success: (res) => resolve(res), + fail: (err) => reject(err) + }) + }) + + console.log('[10] API响应状态:', res.statusCode) + console.log('[11] API响应数据:', JSON.stringify(res.data, null, 2)) + + if (res.statusCode === 200 && res.data.success && res.data.data) { + const homeData = res.data.data + const total = res.data.data.total || 0 + + let activities = [] + if (activeTab === 'featured') { + activities = homeData.featuredActivities || [] + console.log('[12] 精选活动原始数量:', activities.length) + } else if (activeTab === 'free') { + activities = homeData.freeActivities || [] + console.log('[12] 免费活动原始数量:', activities.length) + } else if (activeTab === 'vip') { + activities = homeData.vipActivities || [] + console.log('[12] VIP活动原始数量:', activities.length) + } else if (activeTab === 'svip') { + activities = homeData.svipActivities || [] + console.log('[12] SVIP活动原始数量:', activities.length) + } + + const newActivityList = activities.map(item => { + const heat = item.heat !== undefined && item.heat !== null + ? item.heat + : (item.likesCount || 0) * 2 + (item.viewsCount || 0) + ((item.virtualParticipants || 0) + (item.currentParticipants || 0)) * 3 + + return { + id: item.id, + title: item.title, + date: this.formatDate(item.activityDate), + location: item.location || '', + venue: item.venue || '', + image: item.coverImage || '/images/activity-default.jpg', + bgColor: this.getRandomGradient(), + price: item.priceText || '免费', + priceType: item.priceType || 'free', + likes: item.likesCount || 0, + participants: item.currentParticipants || 0, + maxParticipants: item.maxParticipants || 0, + isLiked: item.isLiked || false, + isSignedUp: item.isSignedUp || false, + signupEnabled: item.signupEnabled !== undefined ? item.signupEnabled : true, + activityGuideQrcode: item.activityGuideQrcode || '', + categoryName: item.categoryName || '', + heat: Math.floor(heat), + participantAvatars: item.participantAvatars || [ + 'https://i.pravatar.cc/100?u=1', + 'https://i.pravatar.cc/100?u=2', + 'https://i.pravatar.cc/100?u=3' + ] + } + }) + + console.log('[13] 转换后活动数量:', newActivityList.length) + + const hasMore = activities.length >= limit && (this.data.activityList.length + activities.length) < total + console.log('[14] hasMore:', hasMore, 'current:', this.data.activityList.length + activities.length, 'total:', total) + + if (isLoadMore) { + this.setData({ + activityList: [...this.data.activityList, ...newActivityList], + loadingMore: false, + hasMore, + page: this.data.page + 1, + total + }) + } else { + this.setData({ + activityList: newActivityList, + hasMore, + total + }) + } + console.log('[15] setData完成,当前页面活动数量:', this.data.activityList.length) + } else { + console.log('[ERROR] API返回失败') + if (isLoadMore) { + this.setData({ loadingMore: false }) + } else { + this.setData({ activityList: [], hasMore: false }) + } + } + } catch (err) { + console.error('[ERROR] 加载活动列表失败:', err) + if (isLoadMore) { + this.setData({ loadingMore: false }) + } else { + this.setData({ activityList: [], loading: false, hasMore: false }) + } + } finally { + if (!isLoadMore) { + this.setData({ loading: false }) + } + console.log('========== 加载完成 ==========') + } + }, + + /** + * 加载模拟数据(降级方案) + */ + loadMockActivities() { + // 使用空数据,等待后端API返回真实数据 + const mockActivities = [] + + this.setData({ activityList: mockActivities }) + }, + + /** + * 格式化日期 + */ + formatDate(dateStr) { + if (!dateStr) return '' + const date = new Date(dateStr) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}年${month}月${day}日` + }, + + /** + * 获取随机渐变色 + */ + getRandomGradient() { + const gradients = [ + 'linear-gradient(135deg, #E8D5F0 0%, #F5E6D3 100%)', + 'linear-gradient(135deg, #D5F0E8 0%, #E6D3F5 100%)', + 'linear-gradient(135deg, #F0D5E8 0%, #D3F5E6 100%)', + 'linear-gradient(135deg, #F5E6D3 0%, #E8D5F0 100%)' + ] + return gradients[Math.floor(Math.random() * gradients.length)] + }, + + /** + * 加载未读消息数 + */ + async loadUnreadCount() { + if (!app.globalData.isLoggedIn) { + this.setData({ totalUnread: 0 }) + return + } + + try { + const res = await api.chat.getConversations() + if (res.success && res.data) { + const totalUnread = res.data.reduce((sum, conv) => sum + (conv.unread_count || 0), 0) + this.setData({ totalUnread }) + } + } catch (err) { + console.log('获取未读消息数失败', err) + } + }, + + /** + * 轮播图切换 + */ + onBannerChange(e) { + // 只在用户手动滑动或自动播放时更新索引 + if (e.detail.source === 'autoplay' || e.detail.source === 'touch') { + this.setData({ currentBannerIndex: e.detail.current }) + } + }, + + /** + * 轮播指示器点击 + */ + onDotTap(e) { + const index = e.currentTarget.dataset.index + // 避免重复设置相同索引 + if (index !== this.data.currentBannerIndex) { + this.setData({ currentBannerIndex: index }) + } + }, + + /** + * 分类点击 + */ + onCategoryTap(e) { + const { id, name } = e.currentTarget.dataset + + // 兴趣搭子跳转到专门页面 + if (id === 1) { + wx.navigateTo({ + url: '/pages/interest-partner/interest-partner' + }) + return + } + + // 同城活动跳转到专门页面 + if (id === 2) { + wx.navigateTo({ + url: '/pages/city-activities/city-activities' + }) + return + } + + // 户外郊游跳转到专门页面 + if (id === 3) { + wx.navigateTo({ + url: '/pages/outdoor-activities/outdoor-activities' + }) + return + } + + // 定制主题跳转到专门页面 + if (id === 4) { + wx.navigateTo({ + url: '/pages/theme-travel/theme-travel' + }) + return + } + + // 快乐学堂跳转到专门页面 + if (id === 5) { + wx.navigateTo({ + url: '/pages/happy-school/happy-school' + }) + return + } + + // 单身聚会跳转到专门页面 + if (id === 6) { + wx.navigateTo({ + url: '/pages/singles-party/singles-party' + }) + return + } + + wx.showToast({ title: `${name}功能开发中`, icon: 'none' }) + // TODO: 跳转到对应分类页面 + }, + + /** + * 切换活动标签 + */ + onTabChange(e) { + const tab = e.currentTarget.dataset.tab + console.log('========== Tab切换开始 ==========') + console.log('[1] 点击的Tab:', tab) + console.log('[2] 当前activeTab:', this.data.activeTab) + + if (tab === this.data.activeTab) { + console.log('[3] Tab未变化,跳过') + return + } + + console.log('[4] 更新activeTab为:', tab) + this.setData({ + activeTab: tab, + activityList: [], + page: 1, + hasMore: true + }) + console.log('[5] 调用loadActivityList()') + this.loadActivityList() + }, + + /** + * 下拉刷新 + */ + onPullDownRefresh() { + this.loadActivityList(false).finally(() => { + wx.stopPullDownRefresh() + }) + }, + + /** + * 上拉加载更多 + */ + onReachBottom() { + if (this.data.hasMore && !this.data.loadingMore && !this.data.loading) { + console.log('[100] 触发上拉加载更多') + this.loadActivityList(true) + } else { + console.log('[101] 不满足加载条件:', { + hasMore: this.data.hasMore, + loadingMore: this.data.loadingMore, + loading: this.data.loading + }) + } + }, + + /** + * 活动卡片点击 + */ + onActivityTap(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/activity-detail/activity-detail?id=${id}` + }) + }, + + /** + * 报名按钮点击 + */ + async onSignUp(e) { + const id = e.currentTarget.dataset.id + const index = e.currentTarget.dataset.index + + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ url: '/pages/login/login' }) + return + } + + const activity = this.data.activityList[index] + + // 检查活动状态:满员或结束时弹出二维码 + const isFull = activity.participants >= activity.maxParticipants && activity.maxParticipants > 0 + const isEnded = activity.status === 'ended' || (activity.endDate && new Date(activity.endDate) < new Date()) + + if (isFull || isEnded) { + const qrCode = activity.activityGuideQrcode || activity.activity_guide_qrcode || this.data.qrcodeImageUrl || 'https://ai-c.maimanji.com/api/common/qrcode?type=group' + this.setData({ + qrcodeImageUrl: qrCode, + showQrcodeModal: true + }) + return + } + + // 如果报名功能已关闭,直接显示二维码 + if (activity.signupEnabled === false) { + if (activity.activityGuideQrcode) { + this.setData({ qrcodeImageUrl: activity.activityGuideQrcode }) + } + this.setData({ showQrcodeModal: true }) + return + } + + try { + if (activity.isSignedUp) { + // 取消报名 + const res = await api.activity.cancelSignup(id) + if (res.success) { + wx.showToast({ title: '已取消报名', icon: 'success' }) + this.setData({ + [`activityList[${index}].isSignedUp`]: false, + [`activityList[${index}].participants`]: res.data.currentParticipants + }) + } + } else { + // 报名 + const userInfo = app.globalData.userInfo || {} + const res = await api.activity.signup(id, { + remark: userInfo.nickname || '', + contactPhone: userInfo.phone || '' + }) + if (res.success) { + wx.showToast({ title: '报名成功', icon: 'success' }) + this.setData({ + [`activityList[${index}].isSignedUp`]: true, + [`activityList[${index}].participants`]: res.data.currentParticipants + }) + } else { + // 检查是否需要显示二维码(后端开关关闭) + if (res.code === 'QR_CODE_REQUIRED') { + if (activity.activityGuideQrcode) { + this.setData({ qrcodeImageUrl: activity.activityGuideQrcode }) + } + this.setData({ showQrcodeModal: true }) + } else if (res.code === 'ACTIVITY_ENDED' || res.error === '活动已结束') { + if (activity.activityGuideQrcode) { + this.setData({ qrcodeImageUrl: activity.activityGuideQrcode }) + } + this.setData({ showQrcodeModal: true }) + wx.showToast({ title: '活动已结束,进群查看更多', icon: 'none' }) + } else { + wx.showToast({ + title: res.error || '报名失败', + icon: 'none' + }) + } + } + } + } catch (err) { + console.error('报名操作失败', err) + const isQrRequired = err && (err.code === 'QR_CODE_REQUIRED' || (err.data && err.data.code === 'QR_CODE_REQUIRED')) + const isActivityEnded = err && (err.code === 'ACTIVITY_ENDED' || (err.data && err.data.code === 'ACTIVITY_ENDED') || err.error === '活动已结束') + + if (isQrRequired || isActivityEnded) { + if (activity.activityGuideQrcode) { + this.setData({ qrcodeImageUrl: activity.activityGuideQrcode }) + } + this.setData({ showQrcodeModal: true }) + if (isActivityEnded) { + wx.showToast({ title: '活动已结束,进群查看更多', icon: 'none' }) + } + } else { + wx.showToast({ + title: err.error || err.message || '操作失败', + icon: 'none' + }) + } + } + }, + + /** + * 关闭二维码弹窗 + */ + onCloseQrcodeModal() { + this.setData({ showQrcodeModal: false }) + }, + + /** + * 保存二维码 + */ + async onSaveQrcode() { + try { + const { qrcodeImageUrl } = this.data + if (!qrcodeImageUrl) { + wx.showToast({ title: '二维码链接不存在', icon: 'none' }) + return + } + + wx.showLoading({ title: '保存中...' }) + + let filePath = '' + + // 判断是否是 Base64 格式 + if (qrcodeImageUrl.startsWith('data:image')) { + const fs = wx.getFileSystemManager() + const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(qrcodeImageUrl) || [] + if (!format || !bodyData) { + throw new Error('Base64 格式错误') + } + filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.${format}` + fs.writeFileSync(filePath, bodyData, 'base64') + } else { + // 远程 URL 格式 + const downloadRes = await new Promise((resolve, reject) => { + wx.downloadFile({ + url: qrcodeImageUrl, + success: resolve, + fail: reject + }) + }) + + if (downloadRes.statusCode !== 200) { + throw new Error('下载图片失败') + } + filePath = downloadRes.tempFilePath + } + + // 保存到相册 + await new Promise((resolve, reject) => { + wx.saveImageToPhotosAlbum({ + filePath: filePath, + success: resolve, + fail: reject + }) + }) + + wx.hideLoading() + wx.showToast({ title: '保存成功', icon: 'success' }) + this.onCloseQrcodeModal() + } catch (err) { + wx.hideLoading() + console.error('保存二维码失败', err) + + if (err.errMsg && (err.errMsg.includes('auth deny') || err.errMsg.includes('auth denied'))) { + wx.showModal({ + title: '需要授权', + content: '请允许访问相册以保存二维码', + confirmText: '去设置', + success: (res) => { + if (res.confirm) { + wx.openSetting() + } + } + }) + } else { + wx.showToast({ title: err.message || '保存失败', icon: 'none' }) + } + } + }, + + /** + * 阻止冒泡 + */ + preventBubble() { + return + }, + + /** + * 点赞 + */ + async onLike(e) { + const id = e.currentTarget.dataset.id + const index = e.currentTarget.dataset.index + + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ url: '/pages/login/login' }) + return + } + + try { + const res = await api.activity.toggleLike(id) + if (res.success) { + this.setData({ + [`activityList[${index}].isLiked`]: res.data.isLiked, + [`activityList[${index}].likes`]: res.data.likesCount + }) + } + } catch (err) { + console.error('点赞失败', err) + wx.showToast({ title: '操作失败', icon: 'none' }) + } + }, + + /** + * Tab bar 导航 + */ + switchTab(e) { + const path = e.currentTarget.dataset.path + + if (path === '/pages/chat/chat') { + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ + url: '/pages/login/login?redirect=' + encodeURIComponent(path) + }) + return + } + } + wx.switchTab({ url: path }) + } +}) diff --git a/pages/entertainment/entertainment.json b/pages/entertainment/entertainment.json new file mode 100644 index 0000000..1aba645 --- /dev/null +++ b/pages/entertainment/entertainment.json @@ -0,0 +1,8 @@ +{ + "usingComponents": { + "app-icon": "/components/icon/icon" + }, + "navigationStyle": "custom", + "enablePullDownRefresh": true, + "backgroundTextStyle": "dark" +} diff --git a/pages/entertainment/entertainment.wxml b/pages/entertainment/entertainment.wxml new file mode 100644 index 0000000..a0af262 --- /dev/null +++ b/pages/entertainment/entertainment.wxml @@ -0,0 +1,230 @@ + + + + + + 休闲文娱 + + + + + + + + + + + + + + + + {{item.name}} + + + + + + + + + + + + + + + + + + + + + + + + + + + 精选活动 + + + 免费活动 + + + VIP活动 + + + SVIP活动 + + + + + + + + + + + + + + + + + + {{item.price}} + + + + + {{item.title}} + + + + {{item.date}} + + + + + {{item.location}} + + + + + {{item.heat}} + + + + + + + + {{item.participants}}人已报名 + + + + + + + + + + 暂无活动 + + + + + 没有更多活动了 ~ + + + + + + + + + + + + 陪伴 + + + + 文娱 + + + + 服务 + + + + + + 消息 + + + + 我的 + + + + + + + + + + + + + 请关注二维码 + 回复“报名”获取活动详情 + + + + + + 长按识别二维码或保存图片 + + + 保存二维码图片 + + + diff --git a/pages/entertainment/entertainment.wxss b/pages/entertainment/entertainment.wxss new file mode 100644 index 0000000..3f696b2 --- /dev/null +++ b/pages/entertainment/entertainment.wxss @@ -0,0 +1,729 @@ +/* 休闲文娱页面样式 - 玫瑰紫版 v3.0 */ +page { + background: #fff; +} + +.page-container { + min-height: 100vh; + background: #fff; + position: relative; +} + +/* 顶部导航栏已移除,改用全局 unified-header */ + +/* 内容滚动区域 */ +.content-scroll { + height: 100vh; + box-sizing: border-box; +} + +.content-scroll::-webkit-scrollbar { + display: none; +} + +/* 顶部轮播区域 - 与服务页保持一致 */ +.banner-section { + padding: 24rpx 32rpx; +} + +.banner-swiper { + width: 100%; + height: 400rpx; + border-radius: 32rpx; + overflow: hidden; + box-shadow: 0 8rpx 24rpx -8rpx rgba(243, 244, 246, 1), 0 20rpx 30rpx -6rpx rgba(243, 244, 246, 1); +} + +/* 防止轮播图闪烁 */ +swiper-item { + will-change: transform; + backface-visibility: hidden; + -webkit-backface-visibility: hidden; +} + +.banner-card { + width: 100%; + height: 100%; + border-radius: 32rpx; + overflow: hidden; + position: relative; + cursor: pointer; + /* 防止闪烁 */ + transform: translateZ(0); + -webkit-transform: translateZ(0); +} + +/* Banner图片 - 优化加载性能 */ +.banner-image { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + /* 防止图片闪烁 */ + backface-visibility: hidden; + -webkit-backface-visibility: hidden; + transform: translateZ(0); + -webkit-transform: translateZ(0); +} + +.banner-bg-gradient { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + background: linear-gradient(135deg, #F5E6ED 0%, #FAF5F8 100%); +} + +.banner-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(0deg, rgba(26, 26, 26, 0.4) 0%, rgba(26, 26, 26, 0) 50%); +} + +/* 轮播指示器已移除 */ + +/* 功能入口 - 与服务页面统一的图标风格 */ +.category-section { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + width: 718rpx; + margin: 0 auto; + padding: 24rpx 0; + row-gap: 64rpx; + background: transparent; +} + +.category-item { + width: 218rpx; + min-height: 208rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + gap: 24rpx; + padding: 0; + box-sizing: border-box; + cursor: pointer; + transition: transform 0.2s ease; +} + +.category-item:active { + transform: scale(0.95); +} + +/* 图标容器:圆形图片 */ +.category-icon-container { + width: 168rpx; + height: 168rpx; + border-radius: 9999rpx; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + overflow: hidden; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* 图标图片 */ +.icon-svg { + width: 168rpx; + height: 168rpx; + border-radius: 9999rpx; +} + +.category-name { + font-family: Arial, sans-serif; + font-size: 36rpx; + font-weight: 700; + color: #364153; + line-height: 1.56; + text-align: center; + width: 218rpx; + transition: color 0.2s ease; +} + +.category-item:active .category-name { + color: #914584; +} + +/* 滚动公告栏 */ +.notice-bar { + margin: 32rpx; + padding: 28rpx 32rpx; + background: #fff; + border-radius: 40rpx; + border: 2rpx solid #F3F4F6; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04); + display: flex; + align-items: center; + gap: 24rpx; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.notice-bar:active { + transform: scale(0.98); + box-shadow: 0 2rpx 12rpx rgba(145, 69, 132, 0.12); +} + +.notice-icon { + width: 72rpx; + height: 72rpx; + background: linear-gradient(135deg, #FEF2F2 0%, #FFE2E2 100%); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + box-shadow: 0 2rpx 8rpx rgba(251, 44, 54, 0.15); + border: 2rpx solid rgba(251, 44, 54, 0.1); +} + +.heart-icon { + width: 44rpx; + height: 44rpx; + filter: drop-shadow(0 1rpx 2rpx rgba(251, 44, 54, 0.2)); +} + +.notice-content { + flex: 1; + height: 56rpx; + overflow: hidden; +} + +.notice-swiper { + height: 56rpx; +} + +.notice-text { + font-size: 36rpx; + font-weight: 600; + color: #1A1A1A; + line-height: 56rpx; +} + +.notice-text rich-text { + line-height: 56rpx; +} + +.notice-bar-empty { + height: 0; + margin: 0 32rpx; +} + +.notice-arrow { + width: 40rpx; + height: 40rpx; + opacity: 0.5; +} + +/* 活动标签切换 - 横向滚动 */ +.tab-section { + padding: 32rpx 0; + background: #fff; + margin: 0 32rpx 32rpx; + border-radius: 48rpx; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04); + position: relative; + z-index: 1; +} + +.tab-scroll { + width: 100%; + white-space: nowrap; +} + +.tab-scroll::-webkit-scrollbar { + display: none; +} + +.tab-list { + display: inline-flex; + gap: 20rpx; + padding: 0 32rpx; +} + +.tab-item { + padding: 20rpx 48rpx; + border-radius: 100rpx; + font-size: 36rpx; + font-weight: 700; + color: #6A7282; + background: #F3F4F6; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + position: relative; + z-index: 2; + flex-shrink: 0; + white-space: nowrap; +} + +.tab-item:active { + transform: scale(0.96); +} + +.tab-item.active { + color: #fff; + background: linear-gradient(135deg, #914584 0%, #7A3A6F 100%); + box-shadow: 0 16rpx 32rpx rgba(145, 69, 132, 0.3); + transform: scale(1.02); +} + +/* 活动列表 */ +.activity-list { + padding: 0 32rpx; +} + +.activity-card { + background: #fff; + border-radius: 48rpx; + margin-bottom: 32rpx; + overflow: hidden; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; +} + +.activity-card:active { + transform: translateY(-2rpx) scale(0.98); + box-shadow: 0 2rpx 12rpx rgba(145, 69, 132, 0.12); +} + +.activity-image-wrap { + width: 100%; + height: 400rpx; + position: relative; + overflow: hidden; + border-radius: 32rpx 32rpx 0 0; +} + +/* 活动实际图片 */ +.activity-image { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + object-fit: cover; +} + +/* 活动图片渐变背景(降级方案) */ +.activity-image-gradient { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + background: linear-gradient(135deg, #F5E6ED 0%, #FAF5F8 100%); +} + +/* 点赞徽章 */ +.like-badge { + position: absolute; + top: 32rpx; + right: 32rpx; + display: flex; + align-items: center; + gap: 12rpx; + padding: 12rpx 28rpx 12rpx 16rpx; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(8px); + border-radius: 100rpx; + box-shadow: 0 4rpx 16rpx rgba(145, 69, 132, 0.12); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; +} + +.like-badge:active { + transform: scale(0.92); +} + +.like-badge.liked { + background: rgba(254, 242, 242, 0.98); + box-shadow: 0 4rpx 16rpx rgba(251, 44, 54, 0.3); +} + +.like-icon { + width: 36rpx; + height: 36rpx; + transition: transform 0.25s cubic-bezier(0.68, -0.55, 0.265, 1.55); +} + +.like-badge.liked .like-icon { + transform: scale(1.25); + animation: heartBeat 0.5s ease; +} + +@keyframes heartBeat { + 0%, 100% { transform: scale(1.25); } + 50% { transform: scale(1.4); } +} + +.like-count { + font-size: 32rpx; + font-weight: 700; + color: #4A5565; + transition: color 0.25s ease; +} + +.like-badge.liked .like-count { + color: #FB2C36; +} + +/* 价格标签 */ +.price-tag { + position: absolute; + bottom: 32rpx; + left: 32rpx; + padding: 12rpx 28rpx; + border-radius: 100rpx; + font-size: 32rpx; + font-weight: 700; + backdrop-filter: blur(4px); +} + +.price-tag.paid { + background: linear-gradient(135deg, #F97316 0%, #EA580C 100%); + color: #fff; + box-shadow: 0 4rpx 16rpx rgba(249, 115, 22, 0.35); +} + +.price-tag.free { + background: linear-gradient(135deg, #4ADE80 0%, #16A34A 100%); + color: #fff; + box-shadow: 0 4rpx 16rpx rgba(74, 222, 128, 0.35); +} + +/* 活动信息 */ +.activity-info { + padding: 40rpx; +} + +.activity-title { + font-size: 44rpx; + font-weight: 700; + color: #1A1A1A; + line-height: 1.3; + margin-bottom: 24rpx; + display: block; +} + +.activity-meta { + display: flex; + flex-direction: column; + gap: 20rpx; + margin-bottom: 24rpx; +} + +.meta-row { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; +} + +.meta-item { + display: flex; + align-items: center; + gap: 20rpx; +} + +.meta-icon { + width: 40rpx; + height: 40rpx; + opacity: 0.6; +} + +.meta-text { + font-size: 32rpx; + color: #4A5565; + font-weight: 500; +} + +/* 热度显示样式 */ +.heat-item { + margin-left: auto; +} + +.heat-text { + color: #F97316; + font-weight: 700; +} + +/* 活动底部 */ +.activity-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 24rpx; + border-top: 3rpx solid #F3F4F6; +} + +.participants { + display: flex; + align-items: center; + gap: 24rpx; +} + +.avatar-stack { + display: flex; +} + +.mini-avatar { + width: 64rpx; + height: 64rpx; + border-radius: 50%; + background: linear-gradient(135deg, #F5E6ED 0%, #FAF5F8 100%); + border: 3rpx solid #fff; + margin-left: -16rpx; +} + +.mini-avatar:first-child { + margin-left: 0; +} + +.participant-text { + font-size: 32rpx; + color: #6A7282; +} + +.signup-btn { + width: 240rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + + background: linear-gradient(135deg, #914584 0%, #7A3A6F 100%); + border-radius: 100rpx; + font-size: 32rpx; + font-weight: 700; + color: #fff; + box-shadow: 0 16rpx 32rpx rgba(145, 69, 132, 0.3); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + white-space: nowrap; + flex-shrink: 0; +} + +.signup-btn:active { + transform: scale(0.96); + box-shadow: 0 8rpx 24rpx rgba(145, 69, 132, 0.4); +} + +.signup-btn.signed { + background: linear-gradient(135deg, #9CA3AF 0%, #6B7280 100%); + box-shadow: 0 4rpx 16rpx rgba(156, 163, 175, 0.25); +} + +/* 空状态 */ +.empty-state { + padding: 200rpx 0; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.empty-icon { + width: 240rpx; + height: 240rpx; + margin: 0 auto 32rpx; + opacity: 0.5; +} + +.empty-text { + font-size: 32rpx; + color: #99A1AF; +} + +/* 列表底部 */ +.list-footer { + padding: 60rpx 0; + text-align: center; +} + +.footer-text { + font-size: 36rpx; + color: #6A7282; +} + +/* 底部占位 */ +.bottom-placeholder { + height: 240rpx; +} + +/* 自定义底部导航栏 */ +.custom-tabbar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 194rpx; + background: #FFFFFF; + display: flex; + align-items: flex-start; + justify-content: space-around; + padding-top: 24rpx; + z-index: 999; + border-top: 2rpx solid #F3F4F6; +} + +.tabbar-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12rpx; + width: 150rpx; + height: 120rpx; +} + +.tabbar-icon { + width: 68rpx; + height: 68rpx; +} + +.tabbar-text { + font-family: Arial, sans-serif; + font-size: 40rpx; + font-weight: 700; + color: #A58AA5; + line-height: 1; +} + +.tabbar-text.active { + color: #B06AB3; +} + +.message-icon-wrapper { + position: relative; + width: 68rpx; + height: 68rpx; +} + +/* 二维码引导弹窗 */ +.qrcode-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + visibility: hidden; + opacity: 0; + transition: all 0.3s ease; +} + +.qrcode-modal.show { + visibility: visible; + opacity: 1; +} + +.qrcode-modal .modal-mask { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4rpx); +} + +.qrcode-modal .modal-content { + position: relative; + width: 600rpx; + background: #FFFFFF; + border-radius: 48rpx; + padding: 60rpx 40rpx; + display: flex; + flex-direction: column; + align-items: center; + z-index: 1; + transform: scale(0.8); + transition: all 0.3s ease; +} + +.qrcode-modal.show .modal-content { + transform: scale(1); +} + +.qrcode-modal .close-btn { + position: absolute; + top: 30rpx; + right: 30rpx; + width: 60rpx; + height: 60rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.qrcode-modal .close-icon { + width: 32rpx; + height: 32rpx; +} + +.qrcode-modal .modal-title { + font-size: 40rpx; + font-weight: bold; + color: #333; + margin-bottom: 12rpx; +} + +.qrcode-modal .modal-subtitle { + font-size: 28rpx; + color: #666; + margin-bottom: 40rpx; +} + +.qrcode-modal .qrcode-container { + width: 400rpx; + height: 400rpx; + background: #f9f9f9; + border: 2rpx solid #eee; + border-radius: 24rpx; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 24rpx; + overflow: hidden; +} + +.qrcode-modal .qrcode-image { + width: 360rpx; + height: 360rpx; +} + +.qrcode-modal .modal-tips { + font-size: 24rpx; + color: #999; + margin-bottom: 40rpx; +} + +.qrcode-modal .save-btn { + width: 100%; + height: 88rpx; + background: #07C160; + color: #fff; + border-radius: 44rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + font-weight: bold; +} + +.qrcode-modal .save-btn:active { + opacity: 0.8; +} diff --git a/pages/gift-detail/gift-detail.js b/pages/gift-detail/gift-detail.js new file mode 100644 index 0000000..8e117d2 --- /dev/null +++ b/pages/gift-detail/gift-detail.js @@ -0,0 +1,185 @@ +const api = require('../../utils/api') +const auth = require('../../utils/auth') +const config = require('../../config/index') + +Page({ + data: { + giftId: '', + gift: null, + userLovePoints: 0, + loading: false + }, + + getGiftImageUrl(url) { + if (!url) return '' + if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('data:')) return url + if (url.startsWith('/images/gifts/')) { + const baseUrl = String(config.API_BASE_URL || '').replace(/\/api$/, '') + return baseUrl + url + } + return url + }, + + async onLoad(options) { + if (!options.id) { + wx.showToast({ title: '礼物ID缺失', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + return + } + + this.setData({ giftId: options.id }) + + // 统一登录验证 + const isValid = await auth.ensureLogin({ + pageName: 'gift-detail', + redirectUrl: `/pages/gift-detail/gift-detail?id=${options.id}` + }) + + if (!isValid) return + + // 验证通过后,稍作延迟确保token稳定 + await new Promise(resolve => setTimeout(resolve, 50)) + + // 加载数据 + this.loadGiftDetail() + this.loadUserLovePoints() + }, + + async loadGiftDetail() { + this.setData({ loading: true }) + + try { + const res = await api.gifts.getDetail(this.data.giftId) + if (res.success) { + const gift = res.data || null + if (gift && gift.image) gift.image = this.getGiftImageUrl(gift.image) + this.setData({ + gift + }) + } + } catch (error) { + console.error('加载礼品详情失败:', error) + // 401错误由API层统一处理,这里只处理其他错误 + if (error.code !== 401) { + wx.showToast({ + title: error.message || '加载失败', + icon: 'none' + }) + } + } finally { + this.setData({ loading: false }) + } + }, + + async loadUserLovePoints() { + try { + const res = await api.loveExchange.getOptions() + if (res.success) { + this.setData({ + userLovePoints: res.data.current_love_points || 0 + }) + } + } catch (error) { + console.error('加载爱心值失败:', error) + // 401错误由API层统一处理 + } + }, + + // 兑换礼品 + async exchangeGift() { + const gift = this.data.gift + + // 检查爱心值是否足够 + if (this.data.userLovePoints < gift.love_cost) { + wx.showModal({ + title: '爱心值不足', + content: `需要 ${gift.love_cost} 爱心值,当前 ${this.data.userLovePoints} 爱心值`, + showCancel: false + }) + return + } + + // 检查库存 + if (gift.stock <= 0) { + wx.showToast({ + title: '库存不足', + icon: 'none' + }) + return + } + + // 获取收货地址 + try { + const address = await wx.chooseAddress() + + // 确认兑换 + wx.showModal({ + title: '确认兑换', + content: `确定使用 ${gift.love_cost} 爱心值兑换${gift.name}吗?`, + success: async (res) => { + if (res.confirm) { + await this.doExchange(address) + } + } + }) + } catch (error) { + if (error.errMsg && error.errMsg.includes('cancel')) { + // 用户取消选择地址 + return + } + console.error('获取地址失败:', error) + wx.showToast({ + title: '请授权收货地址', + icon: 'none' + }) + } + }, + + async doExchange(address) { + wx.showLoading({ title: '兑换中...' }) + + try { + const res = await api.gifts.exchange({ + giftId: this.data.giftId, + shippingInfo: { + name: address.userName, + phone: address.telNumber, + address: `${address.provinceName}${address.cityName}${address.countyName}${address.detailInfo}` + } + }) + + wx.hideLoading() + + if (res.success) { + wx.showModal({ + title: '兑换成功', + content: res.message || '礼品兑换成功,请在兑换记录中查看物流信息', + showCancel: false, + success: () => { + wx.navigateTo({ + url: '/pages/gift-exchanges/gift-exchanges' + }) + } + }) + } + } catch (error) { + wx.hideLoading() + console.error('兑换失败:', error) + wx.showToast({ + title: error.message || '兑换失败', + icon: 'none' + }) + } + }, + + // 预览图片 + previewImage() { + const imageUrl = this.data.gift && (this.data.gift.image || this.data.gift.image_url) + if (imageUrl) { + wx.previewImage({ + urls: [imageUrl], + current: imageUrl + }) + } + } +}) diff --git a/pages/gift-detail/gift-detail.json b/pages/gift-detail/gift-detail.json new file mode 100644 index 0000000..1ce92c4 --- /dev/null +++ b/pages/gift-detail/gift-detail.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "礼品详情", + "navigationBarBackgroundColor": "#F2EDFF", + "navigationBarTextStyle": "black" +} diff --git a/pages/gift-detail/gift-detail.wxml b/pages/gift-detail/gift-detail.wxml new file mode 100644 index 0000000..165584c --- /dev/null +++ b/pages/gift-detail/gift-detail.wxml @@ -0,0 +1,55 @@ + + + + + + + + + + + {{gift.name}} + + + {{gift.love_cost}} + + + + + + 分类: + {{gift.category}} + + + 库存: + {{gift.stock}} + + + + + 商品详情 + {{gift.description}} + + + + + + + 我的爱心值 + {{userLovePoints}} + + + + + + + + 加载中... + + diff --git a/pages/gift-detail/gift-detail.wxss b/pages/gift-detail/gift-detail.wxss new file mode 100644 index 0000000..382a06d --- /dev/null +++ b/pages/gift-detail/gift-detail.wxss @@ -0,0 +1,161 @@ +.gift-detail-container { + min-height: 100vh; + background: #F3F4F6; + padding-bottom: 160rpx; +} + +.image-section { + width: 100%; + height: 600rpx; + background: #FFFFFF; +} + +.gift-image { + width: 100%; + height: 100%; +} + +.info-section { + background: #FFFFFF; + margin-top: 20rpx; + padding: 40rpx; +} + +.gift-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 32rpx; + padding-bottom: 32rpx; + border-bottom: 2rpx solid #E5E7EB; +} + +.gift-name { + flex: 1; + font-size: 40rpx; + font-weight: bold; + color: #1F2937; + margin-right: 32rpx; +} + +.gift-price { + display: flex; + align-items: center; + flex-shrink: 0; +} + +.heart-icon { + width: 40rpx; + height: 40rpx; + margin-right: 8rpx; +} + +.price-text { + font-size: 48rpx; + font-weight: bold; + color: #A78BFA; +} + +.gift-meta { + margin-bottom: 32rpx; +} + +.meta-item { + display: flex; + align-items: center; + margin-bottom: 16rpx; +} + +.meta-label { + font-size: 28rpx; + color: #6B7280; + margin-right: 16rpx; +} + +.meta-value { + font-size: 28rpx; + color: #1F2937; +} + +.gift-description { + margin-top: 32rpx; + padding-top: 32rpx; + border-top: 2rpx solid #E5E7EB; +} + +.description-title { + display: block; + font-size: 32rpx; + font-weight: 600; + color: #1F2937; + margin-bottom: 24rpx; +} + +.description-text { + display: block; + font-size: 28rpx; + color: #6B7280; + line-height: 1.8; +} + +.bottom-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: #FFFFFF; + padding: 24rpx 40rpx; + display: flex; + align-items: center; + box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.05); + z-index: 100; +} + +.balance-info { + flex: 1; + margin-right: 32rpx; +} + +.balance-label { + display: block; + font-size: 24rpx; + color: #6B7280; + margin-bottom: 8rpx; +} + +.balance-value { + display: block; + font-size: 36rpx; + font-weight: bold; + color: #A78BFA; +} + +.exchange-btn { + flex-shrink: 0; + background: linear-gradient(135deg, #A78BFA 0%, #C084FC 100%); + color: #FFFFFF; + border-radius: 50rpx; + font-size: 32rpx; + padding: 24rpx 64rpx; + box-shadow: 0 4rpx 12rpx rgba(167, 139, 250, 0.3); +} + +.exchange-btn::after { + border: none; +} + +.exchange-btn.disabled { + background: #E5E7EB; + color: #9CA3AF; + box-shadow: none; +} + +.loading-state { + padding: 200rpx 0; + text-align: center; +} + +.loading-text { + font-size: 28rpx; + color: #9CA3AF; +} diff --git a/pages/gift-exchanges/gift-exchanges.js b/pages/gift-exchanges/gift-exchanges.js new file mode 100644 index 0000000..75c2fec --- /dev/null +++ b/pages/gift-exchanges/gift-exchanges.js @@ -0,0 +1,113 @@ +const api = require('../../utils/api') +const auth = require('../../utils/auth') + +Page({ + data: { + exchanges: [], + loading: false + }, + + async onLoad() { + // 统一登录验证 + const isValid = await auth.ensureLogin({ + pageName: 'gift-exchanges', + redirectUrl: '/pages/gift-exchanges/gift-exchanges' + }) + + if (!isValid) return + + // 验证通过后,稍作延迟确保token稳定 + await new Promise(resolve => setTimeout(resolve, 50)) + + // 加载数据 + this.loadExchanges() + }, + + onShow() { + // 每次显示页面时刷新数据(已登录的情况下) + const app = getApp() + if (app.globalData.isLoggedIn) { + this.loadExchanges() + } + }, + + async loadExchanges() { + if (this.data.loading) return + + this.setData({ loading: true }) + + try { + const res = await api.gifts.getMyExchanges({ limit: 50 }) + if (res.success) { + this.setData({ + exchanges: res.data + }) + } + } catch (error) { + console.error('加载兑换记录失败:', error) + // 401错误由API层统一处理,这里只处理其他错误 + if (error.code !== 401) { + wx.showToast({ + title: error.message || '加载失败', + icon: 'none' + }) + } + } finally { + this.setData({ loading: false }) + } + }, + + // 查看物流 + viewTracking(e) { + const trackingNumber = e.currentTarget.dataset.tracking + if (!trackingNumber) { + wx.showToast({ + title: '暂无物流信息', + icon: 'none' + }) + return + } + + // 复制物流单号 + wx.setClipboardData({ + data: trackingNumber, + success: () => { + wx.showToast({ + title: '物流单号已复制', + icon: 'success' + }) + } + }) + }, + + // 格式化状态 + formatStatus(status) { + const statusMap = { + 'pending': '待发货', + 'shipped': '已发货', + 'delivered': '已送达', + 'cancelled': '已取消' + } + return statusMap[status] || status + }, + + // 格式化时间 + formatTime(dateStr) { + const date = new Date(dateStr) + return date.toLocaleDateString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }) + }, + + // 下拉刷新 + onPullDownRefresh() { + this.loadExchanges() + setTimeout(() => { + wx.stopPullDownRefresh() + }, 1000) + } +}) diff --git a/pages/gift-exchanges/gift-exchanges.json b/pages/gift-exchanges/gift-exchanges.json new file mode 100644 index 0000000..67a840c --- /dev/null +++ b/pages/gift-exchanges/gift-exchanges.json @@ -0,0 +1,7 @@ +{ + "navigationBarTitleText": "兑换记录", + "navigationBarBackgroundColor": "#F2EDFF", + "navigationBarTextStyle": "black", + "enablePullDownRefresh": true, + "backgroundColor": "#F3F4F6" +} diff --git a/pages/gift-exchanges/gift-exchanges.wxml b/pages/gift-exchanges/gift-exchanges.wxml new file mode 100644 index 0000000..c624186 --- /dev/null +++ b/pages/gift-exchanges/gift-exchanges.wxml @@ -0,0 +1,61 @@ + + + + + 订单号:{{item.order_number}} + {{formatStatus(item.status)}} + + + + + + {{item.gift_name}} + + + {{item.love_cost}} + + + + + + + 收货人: + {{item.shipping_name}} + + + 联系电话: + {{item.shipping_phone}} + + + 收货地址: + {{item.shipping_address}} + + + 物流单号: + + {{item.tracking_number}} + + + + 兑换时间:{{formatTime(item.created_at)}} + + + + + + + + + 暂无兑换记录 + + + + + + 加载中... + + diff --git a/pages/gift-exchanges/gift-exchanges.wxss b/pages/gift-exchanges/gift-exchanges.wxss new file mode 100644 index 0000000..8c6548d --- /dev/null +++ b/pages/gift-exchanges/gift-exchanges.wxss @@ -0,0 +1,185 @@ +.exchanges-container { + min-height: 100vh; + background: #F3F4F6; + padding: 40rpx; +} + +.exchanges-list { + display: flex; + flex-direction: column; + gap: 24rpx; +} + +.exchange-card { + background: #FFFFFF; + border-radius: 16rpx; + overflow: hidden; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 24rpx 32rpx; + background: #F9FAFB; + border-bottom: 2rpx solid #E5E7EB; +} + +.order-number { + font-size: 26rpx; + color: #6B7280; +} + +.status { + font-size: 26rpx; + font-weight: 500; + padding: 8rpx 20rpx; + border-radius: 20rpx; +} + +.status.pending { + background: #FEF3C7; + color: #D97706; +} + +.status.shipped { + background: #DBEAFE; + color: #2563EB; +} + +.status.delivered { + background: #D1FAE5; + color: #059669; +} + +.status.cancelled { + background: #FEE2E2; + color: #DC2626; +} + +.card-body { + display: flex; + padding: 32rpx; + border-bottom: 2rpx solid #E5E7EB; +} + +.gift-image { + width: 160rpx; + height: 160rpx; + border-radius: 12rpx; + background: #F3F4F6; + flex-shrink: 0; +} + +.gift-info { + flex: 1; + margin-left: 24rpx; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.gift-name { + font-size: 32rpx; + color: #1F2937; + font-weight: 500; +} + +.gift-cost { + display: flex; + align-items: center; +} + +.heart-icon { + width: 32rpx; + height: 32rpx; + margin-right: 8rpx; +} + +.cost-text { + font-size: 32rpx; + font-weight: bold; + color: #A78BFA; +} + +.card-footer { + padding: 24rpx 32rpx; +} + +.shipping-info { + display: flex; + margin-bottom: 16rpx; +} + +.info-label { + font-size: 26rpx; + color: #6B7280; + flex-shrink: 0; + width: 140rpx; +} + +.info-value { + flex: 1; + font-size: 26rpx; + color: #1F2937; +} + +.info-value.tracking { + color: #A78BFA; + text-decoration: underline; +} + +.exchange-time { + margin-top: 16rpx; + padding-top: 16rpx; + border-top: 2rpx solid #E5E7EB; +} + +.time-text { + font-size: 24rpx; + color: #9CA3AF; +} + +.empty-state { + padding: 200rpx 0; + text-align: center; +} + +.empty-icon { + width: 200rpx; + height: 200rpx; + margin-bottom: 32rpx; + opacity: 0.5; +} + +.empty-text { + display: block; + font-size: 28rpx; + color: #9CA3AF; + margin-bottom: 40rpx; +} + +.goto-shop-btn { + background: linear-gradient(135deg, #A78BFA 0%, #C084FC 100%); + color: #FFFFFF; + border-radius: 50rpx; + font-size: 32rpx; + padding: 24rpx 64rpx; + width: 400rpx; + margin: 0 auto; +} + +.goto-shop-btn::after { + border: none; +} + +.loading-state { + padding: 80rpx 0; + text-align: center; +} + +.loading-text { + font-size: 28rpx; + color: #9CA3AF; +} diff --git a/pages/gift-shop/gift-shop.js b/pages/gift-shop/gift-shop.js new file mode 100644 index 0000000..25cdf27 --- /dev/null +++ b/pages/gift-shop/gift-shop.js @@ -0,0 +1,141 @@ +const api = require('../../utils/api') +const auth = require('../../utils/auth') +const config = require('../../config/index') + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + lovePoints: 0, + gifts: [], + giftsLoading: false, + giftsError: '', + skeletonGifts: Array.from({ length: 6 }).map((_, idx) => ({ id: idx + 1 })) + }, + + getGiftImageUrl(url) { + if (!url) return '' + if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('data:')) return url + // 兼容所有相对路径 + if (url.startsWith('/')) { + const baseUrl = String(config.API_BASE_URL || '').replace(/\/api$/, '') + return baseUrl + url + } + return url + }, + + async onLoad() { + // 计算导航栏高度 + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight + }) + + this.loadGifts() + + const isValid = await auth.ensureLogin({ + pageName: 'gift-shop', + redirectUrl: '/pages/gift-shop/gift-shop' + }) + + if (!isValid) return + + await new Promise(resolve => setTimeout(resolve, 50)) + this.loadLovePoints() + }, + + onShow() { + const app = getApp() + if (app.globalData.isLoggedIn) { + this.loadLovePoints() + } + }, + + // 加载爱心值 + async loadLovePoints() { + try { + const res = await api.loveExchange.getOptions() + if (res.success) { + this.setData({ + lovePoints: res.data.current_love_points || 0 + }) + } + } catch (error) { + console.error('加载爱心值失败:', error) + } + }, + + async loadGifts() { + if (this.data.giftsLoading) return + + this.setData({ + giftsLoading: true, + giftsError: '' + }) + + try { + // 使用 shop.getItems 获取 exchange_items 数据 + const res = await api.shop.getItems() + if (res.success) { + const gifts = (res.data || []).map(gift => ({ + id: gift.id, + name: gift.name, + // 兼容后端返回 image 或 image_url + image: this.getGiftImageUrl(gift.image_url || gift.image), + // 兼容后端返回 price 或 love_cost + love_cost: gift.price || gift.love_cost, + stock: gift.stock, + sold_count: gift.sold_count, + category: gift.category + })) + this.setData({ gifts }) + } else { + this.setData({ giftsError: res.error || '获取礼品失败' }) + } + } catch (error) { + console.error('加载礼品失败:', error) + this.setData({ giftsError: error.message || '获取礼品失败' }) + } finally { + this.setData({ giftsLoading: false }) + } + }, + + // 返回上一页 + goBack() { + wx.navigateBack() + }, + + // 查看明细 + onViewDetails() { + wx.navigateTo({ + url: '/pages/love-transactions/love-transactions' + }) + }, + + // 点击礼品卡片 + onGiftTap(e) { + wx.showModal({ + title: '提示', + content: '请下载app端进行兑换', + showCancel: false, + confirmText: '知道了' + }) + }, + + // 下拉刷新 + onPullDownRefresh() { + this.loadLovePoints() + this.loadGifts() + setTimeout(() => { + wx.stopPullDownRefresh() + }, 1000) + } +}) diff --git a/pages/gift-shop/gift-shop.json b/pages/gift-shop/gift-shop.json new file mode 100644 index 0000000..ad7cf18 --- /dev/null +++ b/pages/gift-shop/gift-shop.json @@ -0,0 +1,5 @@ +{ + "navigationStyle": "custom", + "enablePullDownRefresh": true, + "backgroundColor": "#FAF8FC" +} diff --git a/pages/gift-shop/gift-shop.wxml b/pages/gift-shop/gift-shop.wxml new file mode 100644 index 0000000..a3e6271 --- /dev/null +++ b/pages/gift-shop/gift-shop.wxml @@ -0,0 +1,93 @@ + + + + + + + + + 礼品商城 + + + + + + + + + + + + + + + + 我的爱心 + + {{lovePoints}} + + 点击查看明细 + + + + + + + + + + + + + + 超值兑换 + + + + + + + + + + + + + + + {{giftsError}} + 重试 + + + + + 暂无可兑换礼品 + + + + + + + 热兑 + + + {{item.name}} + + + + + {{item.love_cost}} + + 兑换 + + + + + + + + 更多精美礼品持续上新中... + + + + diff --git a/pages/gift-shop/gift-shop.wxss b/pages/gift-shop/gift-shop.wxss new file mode 100644 index 0000000..f876e27 --- /dev/null +++ b/pages/gift-shop/gift-shop.wxss @@ -0,0 +1,387 @@ +.page-container { + min-height: 100vh; + background: #FAF8FC; +} + +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; + background: transparent; +} + +.status-bar { + background: transparent; +} + +.nav-bar { + display: flex; + align-items: center; + justify-content: center; + padding: 0 32rpx; + background: transparent; + position: relative; +} + +.nav-back { + position: absolute; + left: 32rpx; + top: 50%; + transform: translateY(-50%); + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.back-icon { + width: 48rpx; + height: 48rpx; +} + +.nav-title { + font-size: 36rpx; + font-weight: 700; + color: #101828; + line-height: 1; +} + +.content-scroll { + height: 100vh; + box-sizing: border-box; +} + +.content-wrap { + padding: 30rpx 31rpx 0; + box-sizing: border-box; +} + +.love-card { + width: 688rpx; + height: 313rpx; + border-radius: 46rpx; + overflow: hidden; + position: relative; + box-shadow: 0px 8px 10px -6px rgba(252, 206, 232, 0.5), 0px 20px 25px -5px rgba(252, 206, 232, 0.5); +} + +.love-card-bg { + position: absolute; + inset: 0; + background: linear-gradient(180deg, rgba(176, 106, 179, 1) 0%, rgba(212, 137, 190, 1) 100%); +} + +.love-card-blur { + position: absolute; + width: 366rpx; + height: 366rpx; + top: -92rpx; + right: -92rpx; + border-radius: 9999rpx; + background: rgba(255, 255, 255, 0.1); + filter: blur(128px); +} + +.love-card-bottom-shade { + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 156rpx; + background: linear-gradient(0deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0) 100%); +} + +.love-card-content { + position: relative; + height: 100%; + padding: 46rpx; + display: flex; + align-items: center; + justify-content: space-between; + box-sizing: border-box; +} + +.love-info { + display: flex; + flex-direction: column; + justify-content: center; +} + +.love-header { + display: flex; + align-items: center; + gap: 15rpx; +} + +.love-icon { + width: 61rpx; + height: 61rpx; +} + +.love-label { + font-size: 38rpx; + font-weight: 700; + line-height: 53rpx; + letter-spacing: 1rpx; + color: #FFFFFF; +} + +.love-value { + margin-top: 8rpx; + font-size: 92rpx; + font-weight: 900; + line-height: 92rpx; + letter-spacing: -2rpx; + color: #FFFFFF; + text-shadow: 0px 1px 4px rgba(0, 0, 0, 0.15); +} + +.love-action { + margin-top: 6rpx; + display: flex; + align-items: center; + gap: 8rpx; + opacity: 0.8; +} + +.love-action-text { + font-size: 31rpx; + font-weight: 700; + line-height: 47rpx; + color: #FFFFFF; +} + +.love-action-arrow { + width: 30rpx; + height: 30rpx; +} + +.love-decoration { + width: 134rpx; + height: 134rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.decoration-icon { + width: 134rpx; + height: 134rpx; +} + +.gifts-section { + margin-top: 30rpx; +} + +.gifts-header { + display: flex; + align-items: center; + gap: 15rpx; + padding-left: 8rpx; +} + +.gifts-icon { + width: 46rpx; + height: 46rpx; +} + +.gifts-title { + font-size: 46rpx; + font-weight: 700; + color: #101828; + line-height: 61rpx; +} + +.gifts-grid { + margin-top: 30rpx; + display: grid; + grid-template-columns: 333rpx 333rpx; + gap: 23rpx; +} + +.gift-card { + width: 333rpx; + height: 485rpx; + background: #FFFFFF; + border: 2rpx solid #F9FAFB; + border-radius: 31rpx; + box-shadow: 0px 1px 2px -1px rgba(0, 0, 0, 0.1), 0px 1px 3px 0px rgba(0, 0, 0, 0.1); + padding: 25rpx; + box-sizing: border-box; + display: flex; + flex-direction: column; +} + +.gift-image-wrap { + width: 283rpx; + height: 283rpx; + background: #F9FAFB; + border-radius: 27rpx; + overflow: hidden; + position: relative; +} + +.gift-image { + width: 100%; + height: 100%; +} + +.gift-image-overlay { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.05); +} + +.gift-badge { + position: absolute; + right: 15rpx; + bottom: 15rpx; + width: 76rpx; + height: 38rpx; + border-radius: 9999rpx; + background: rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; + font-size: 23rpx; + font-weight: 700; + line-height: 31rpx; + color: #FFFFFF; +} + +.gift-name { + margin-top: 23rpx; + font-size: 34rpx; + font-weight: 700; + line-height: 53rpx; + color: #101828; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.gift-footer { + margin-top: 15rpx; + display: flex; + align-items: center; + justify-content: space-between; +} + +.gift-price { + display: flex; + align-items: center; + gap: 8rpx; +} + +.gift-heart-icon { + width: 34rpx; + height: 34rpx; +} + +.gift-price-text { + font-size: 34rpx; + font-weight: 900; + line-height: 53rpx; + color: #B06AB3; +} + +.gift-exchange-btn { + width: 99rpx; + height: 61rpx; + border-radius: 19rpx; + background: #B06AB3; + display: flex; + align-items: center; + justify-content: center; + font-size: 27rpx; + font-weight: 700; + line-height: 38rpx; + color: #FFFFFF; + box-shadow: 0px 2px 4px -2px rgba(176, 106, 179, 0.2), 0px 4px 6px -1px rgba(176, 106, 179, 0.2); +} + +.gifts-error { + margin-top: 30rpx; + padding: 32rpx; + border-radius: 24rpx; + background: #FFFFFF; + border: 2rpx solid #F9FAFB; +} + +.gifts-empty { + margin-top: 30rpx; + padding: 60rpx 32rpx; + border-radius: 24rpx; + background: #FFFFFF; + border: 2rpx solid #F9FAFB; + text-align: center; +} + +.gifts-empty-text { + font-size: 28rpx; + color: #99A1AF; +} + +.gifts-error-text { + font-size: 28rpx; + color: #6B7280; +} + +.gifts-retry { + margin-top: 18rpx; + width: 160rpx; + height: 60rpx; + border-radius: 19rpx; + background: #B06AB3; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + font-weight: 700; + color: #FFFFFF; +} + +.gift-card.skeleton { + box-shadow: none; +} + +.gift-card.skeleton .gift-image-wrap { + background: #F3F4F6; +} + +.gift-name.skeleton-line, +.gift-price.skeleton-line { + height: 28rpx; + border-radius: 14rpx; + background: #F3F4F6; +} + +.gift-name.skeleton-line { + margin-top: 23rpx; +} + +.gift-price.skeleton-line { + width: 140rpx; +} + +.gift-exchange-btn.skeleton-btn { + width: 99rpx; + height: 61rpx; + background: #F3F4F6; + box-shadow: none; +} + +.bottom-tip { + padding: 60rpx 0 40rpx; + text-align: center; +} + +.tip-text { + font-size: 27rpx; + line-height: 40rpx; + color: #99A1AF; +} diff --git a/pages/happy-school/happy-school.js b/pages/happy-school/happy-school.js new file mode 100644 index 0000000..440d5e0 --- /dev/null +++ b/pages/happy-school/happy-school.js @@ -0,0 +1,394 @@ +// pages/happy-school/happy-school.js - 快乐学堂页面 +const api = require('../../utils/api') +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + loading: false, + loadingMore: false, + activeTab: 'featured', + + // 活动列表 + activityList: [], + + // 分页相关 + page: 1, + limit: 20, + hasMore: true, + total: 0, + + // 二维码弹窗 + showQrcodeModal: false, + qrcodeImageUrl: 'https://ai-c.maimanji.com/images/qrcode-happy-school.jpg' + }, + + onLoad() { + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight + }) + + this.loadActivityList() + }, + + /** + * 返回上一页 + */ + onBack() { + wx.navigateBack() + }, + + /** + * 切换活动标签 + */ + onTabChange(e) { + const tab = e.currentTarget.dataset.tab + if (tab === this.data.activeTab) return + + this.setData({ + activeTab: tab, + activityList: [], + page: 1, + hasMore: true + }) + this.loadActivityList() + }, + + /** + * 下拉刷新 + */ + onPullDownRefresh() { + this.loadActivityList(false).finally(() => { + wx.stopPullDownRefresh() + }) + }, + + /** + * 上拉加载更多 + */ + onReachBottom() { + if (this.data.hasMore && !this.data.loadingMore && !this.data.loading) { + this.loadActivityList(true) + } + }, + + /** + * 加载活动列表 - 根据categoryName筛选快乐学堂(支持分页) + */ + async loadActivityList(isLoadMore = false) { + if (isLoadMore) { + this.setData({ loadingMore: true }) + } else { + this.setData({ loading: true, page: 1, hasMore: true, activityList: [] }) + } + + try { + const { activeTab, page, limit } = this.data + const params = { + category: 'school', + limit: limit, + page: page + } + + if (activeTab === 'featured') { + params.tab = 'featured' + } else if (activeTab === 'free') { + params.priceType = 'free' + } else if (activeTab === 'vip') { + params.is_vip = true + } else if (activeTab === 'svip') { + params.is_svip = true + } + + const res = await api.activity.getList(params) + + if (res.success && res.data && res.data.list) { + const total = res.data.total || 0 + const allActivities = res.data.list + const schoolActivities = allActivities.filter(item => item.categoryName === '快乐学堂') + + let clubQrcode = '' + const firstWithQrcode = schoolActivities.find(item => item.activityGuideQrcode || item.activity_guide_qrcode) + if (firstWithQrcode && !isLoadMore) { + clubQrcode = firstWithQrcode.activityGuideQrcode || firstWithQrcode.activity_guide_qrcode + } + + const newActivityList = schoolActivities.map(item => { + const heat = item.heat || (item.likes * 2 + (item.views || 0) + (item.current_participants || 0) * 3) + + return { + id: item.id, + title: item.title, + date: this.formatDate(item.start_date || item.activityDate), + location: item.location || '', + venue: item.venue || '', + image: item.coverImage || item.cover_image || '', + heat: Math.floor(heat), + price: item.price_text || item.priceText || '免费', + priceType: item.is_free || item.priceType === 'free' ? 'free' : 'paid', + likes: item.likes || item.likesCount || 0, + participants: item.current_participants || item.currentParticipants || 0, + isLiked: item.is_liked || item.isLiked || false, + isSignedUp: item.is_registered || item.isSignedUp || false, + status: item.status || (item.currentParticipants >= item.maxParticipants && item.maxParticipants > 0 ? 'full' : 'upcoming'), + activityGuideQrcode: item.activityGuideQrcode || item.activity_guide_qrcode || '' + } + }) + + const hasMore = newActivityList.length >= limit && (this.data.activityList.length + newActivityList.length) < total + + if (isLoadMore) { + this.setData({ + activityList: [...this.data.activityList, ...newActivityList], + loadingMore: false, + hasMore, + page: this.data.page + 1, + total + }) + } else { + this.setData({ + activityList: newActivityList, + hasMore, + total, + qrcodeImageUrl: clubQrcode || this.data.qrcodeImageUrl + }) + } + + console.log('[happy-school] 加载成功,总数:', total, '当前:', this.data.activityList.length, 'hasMore:', hasMore) + } else { + if (isLoadMore) { + this.setData({ loadingMore: false, hasMore: false }) + } else { + this.setData({ activityList: [], hasMore: false }) + } + } + } catch (err) { + console.error('加载活动列表失败', err) + if (isLoadMore) { + this.setData({ loadingMore: false }) + } else { + this.setData({ activityList: [], loading: false }) + } + } finally { + if (!isLoadMore) { + this.setData({ loading: false }) + } + } + }, + + /** + * 格式化日期 + */ + formatDate(dateStr) { + if (!dateStr) return '' + const date = new Date(dateStr) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}年${month}月${day}日` + }, + + /** + * 活动卡片点击 + */ + onActivityTap(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/activity-detail/activity-detail?id=${id}` + }) + }, + + /** + * 点赞 + */ + async onLike(e) { + const id = e.currentTarget.dataset.id + const index = e.currentTarget.dataset.index + + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ url: '/pages/login/login' }) + return + } + + try { + const res = await api.activity.toggleLike(id) + if (res.success) { + this.setData({ + [`activityList[${index}].isLiked`]: res.data.isLiked, + [`activityList[${index}].likes`]: res.data.likesCount + }) + } + } catch (err) { + console.error('点赞失败', err) + wx.showToast({ title: '操作失败', icon: 'none' }) + } + }, + + /** + * 报名按钮点击 + */ + async onSignUp(e) { + const id = e.currentTarget.dataset.id + const index = e.currentTarget.dataset.index + + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ url: '/pages/login/login' }) + return + } + + const activity = this.data.activityList[index] + + // 检查活动状态 + if (activity.status === 'full' || activity.status === 'ended') { + const qrCode = activity.activityGuideQrcode || activity.activity_guide_qrcode || this.data.qrcodeImageUrl || 'https://ai-c.maimanji.com/api/common/qrcode?type=group' + this.setData({ + qrcodeImageUrl: qrCode, + showQrcodeModal: true + }) + return + } + + try { + if (activity.isSignedUp) { + // 取消报名 + const res = await api.activity.cancelSignup(id) + if (res.success) { + wx.showToast({ title: '已取消报名', icon: 'success' }) + this.loadActivityList() // 刷新列表获取最新人数 + } + } else { + // 报名 + const res = await api.activity.signup(id) + if (res.success) { + wx.showToast({ title: '报名成功', icon: 'success' }) + this.loadActivityList() // 刷新列表获取最新人数 + } else { + // 检查是否需要显示二维码(后端开关关闭或活动已结束) + if (res.code === 'QR_CODE_REQUIRED' || res.error === 'QR_CODE_REQUIRED' || res.code === 'ACTIVITY_ENDED' || res.error === '活动已结束') { + if (activity.activityGuideQrcode || activity.activity_guide_qrcode) { + this.setData({ qrcodeImageUrl: activity.activityGuideQrcode || activity.activity_guide_qrcode }) + } + this.setData({ showQrcodeModal: true }) + if (res.code === 'ACTIVITY_ENDED' || res.error === '活动已结束') { + wx.showToast({ title: '活动已结束,进群查看更多', icon: 'none' }) + } + } else { + wx.showToast({ + title: res.error || '报名失败', + icon: 'none' + }) + } + } + } + } catch (err) { + console.error('报名操作失败', err) + // 捕获特定错误码以显示二维码 + const isQrRequired = err && (err.code === 'QR_CODE_REQUIRED' || (err.data && err.data.code === 'QR_CODE_REQUIRED')) + const isActivityEnded = err && (err.code === 'ACTIVITY_ENDED' || (err.data && err.data.code === 'ACTIVITY_ENDED') || err.error === '活动已结束') + + if (isQrRequired || isActivityEnded) { + if (activity.activityGuideQrcode || activity.activity_guide_qrcode) { + this.setData({ qrcodeImageUrl: activity.activityGuideQrcode || activity.activity_guide_qrcode }) + } + this.setData({ showQrcodeModal: true }) + if (isActivityEnded) { + wx.showToast({ title: '活动已结束,进群查看更多', icon: 'none' }) + } + } else { + wx.showToast({ + title: err.error || err.message || '操作失败', + icon: 'none' + }) + } + } + }, + + /** + * 加入群组 + */ + onJoinGroup() { + // 如果没有二维码,尝试获取第一个活动的二维码 + if (!this.data.qrcodeImageUrl && this.data.activityList.length > 0) { + const firstWithQrcode = this.data.activityList.find(item => item.activityGuideQrcode || item.activity_guide_qrcode) + if (firstWithQrcode) { + this.setData({ qrcodeImageUrl: firstWithQrcode.activityGuideQrcode || firstWithQrcode.activity_guide_qrcode }) + } + } + this.setData({ showQrcodeModal: true }) + }, + + /** + * 阻止冒泡 + */ + preventBubble() { + return + }, + + /** + * 关闭二维码弹窗 + */ + onCloseQrcodeModal() { + this.setData({ showQrcodeModal: false }) + }, + + /** + * 保存二维码 + */ + async onSaveQrcode() { + try { + const { qrcodeImageUrl } = this.data + + // 下载图片到本地 + const downloadRes = await new Promise((resolve, reject) => { + wx.downloadFile({ + url: qrcodeImageUrl, + success: resolve, + fail: reject + }) + }) + + if (downloadRes.statusCode !== 200) { + throw new Error('下载失败') + } + + // 保存到相册 + await new Promise((resolve, reject) => { + wx.saveImageToPhotosAlbum({ + filePath: downloadRes.tempFilePath, + success: resolve, + fail: reject + }) + }) + + wx.showToast({ title: '保存成功', icon: 'success' }) + this.onCloseQrcodeModal() + } catch (err) { + console.error('保存二维码失败', err) + + if (err.errMsg && err.errMsg.includes('auth deny')) { + wx.showModal({ + title: '需要授权', + content: '请允许访问相册以保存二维码', + confirmText: '去设置', + success: (res) => { + if (res.confirm) { + wx.openSetting() + } + } + }) + } else { + wx.showToast({ title: '保存失败', icon: 'none' }) + } + } + } +}) diff --git a/pages/happy-school/happy-school.json b/pages/happy-school/happy-school.json new file mode 100644 index 0000000..ca99e9f --- /dev/null +++ b/pages/happy-school/happy-school.json @@ -0,0 +1,9 @@ +{ + "navigationStyle": "custom", + "enablePullDownRefresh": true, + "backgroundTextStyle": "dark", + "backgroundColor": "#FFF9F0", + "usingComponents": { + "app-icon": "../../components/icon/icon" + } +} diff --git a/pages/happy-school/happy-school.wxml b/pages/happy-school/happy-school.wxml new file mode 100644 index 0000000..b1312b0 --- /dev/null +++ b/pages/happy-school/happy-school.wxml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + 快乐学堂 + + + + + + + + + 快乐学堂俱乐部 + + 终身学习 + 文化传承 + + + + 点击立即加入 + + + + + + + + + 精选活动 + + + 免费活动 + + + VIP活动 + + + SVIP活动 + + + + + + + + + + + + + + + + + {{item.price}} + + + + + {{item.title}} + + + + {{item.date}} + + + + + {{item.location}} + + + + + {{item.heat}} + + + + + + + + {{item.participants}}人已报名 + + + + + + + + + + 暂无课程 + + + + + 没有更多课程了 ~ + + + + + + + + + + + + + + + + + + + + + 加入快乐学堂群 + + + 活到老,学到老,快乐每一天 + + + + + + + + 长按二维码识别或保存 + + + + 保存二维码 + + + + diff --git a/pages/happy-school/happy-school.wxss b/pages/happy-school/happy-school.wxss new file mode 100644 index 0000000..45679dd --- /dev/null +++ b/pages/happy-school/happy-school.wxss @@ -0,0 +1,486 @@ +/* 快乐学堂页面样式 - 阳光橙黄主题 */ +page { + background: linear-gradient(180deg, #FFF4E6 0%, #FFF9F0 100%); +} + +.page-container { + min-height: 100vh; + background: linear-gradient(180deg, #FFF4E6 0%, #FFF9F0 100%); + position: relative; +} + +/* 固定导航栏容器 */ +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; + background: rgba(248, 249, 252, 0.75); + backdrop-filter: blur(20rpx) saturate(180%); + -webkit-backdrop-filter: blur(20rpx) saturate(180%); +} + +/* 状态栏 */ +.status-bar { + background: transparent; +} + +/* 导航栏 */ +.nav-bar { + display: flex; + align-items: center; + justify-content: center; + padding: 0 32rpx; + background: transparent; + position: relative; +} + +.nav-back { + position: absolute; + left: 32rpx; + top: 50%; + transform: translateY(-50%); + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.back-icon { + width: 48rpx; + height: 48rpx; +} + +.nav-title { + font-size: 36rpx; + font-weight: 700; + color: #1A1A1A; + letter-spacing: 1.8%; + line-height: 1; +} + +/* 内容滚动区域 */ +.content-scroll { + height: 100vh; + box-sizing: border-box; +} + +.content-scroll::-webkit-scrollbar { + display: none; +} + +/* 推广卡片 - 阳光渐变 */ +.city-group-card { + margin: 32rpx; + padding: 32rpx 40rpx; + min-height: 128rpx; + background: linear-gradient(135deg, + rgba(255, 244, 230, 0.6) 0%, + rgba(255, 249, 240, 0.6) 100%); + backdrop-filter: blur(16rpx) saturate(150%); + border: 2rpx solid rgba(255, 159, 67, 0.3); + border-radius: 48rpx; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 4rpx 20rpx rgba(255, 159, 67, 0.12); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.city-group-card:active { + transform: scale(0.98); + box-shadow: 0 2rpx 12rpx rgba(255, 159, 67, 0.18); +} + +.group-info { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8rpx; + padding-right: 24rpx; +} + +.group-title { + font-size: 40rpx; + font-weight: 700; + color: #1A1A1A; + line-height: 1.4; + white-space: nowrap; +} + +.group-tags { + display: flex; + flex-direction: column; + gap: 4rpx; +} + +.tag-item { + font-size: 28rpx; + font-weight: 500; + color: #4A5565; + line-height: 1.4; + white-space: nowrap; +} + +.join-btn { + padding: 0 40rpx; + height: 88rpx; + background: linear-gradient(135deg, #FF9F43 0%, #FFBE76 100%); + border-radius: 100rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + font-weight: 700; + color: #fff; + white-space: nowrap; + flex-shrink: 0; + box-shadow: 0 6rpx 24rpx rgba(255, 159, 67, 0.4), + 0 3rpx 12rpx rgba(255, 159, 67, 0.3); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; +} + +.join-btn:active { + transform: scale(0.96); + box-shadow: 0 4rpx 16rpx rgba(255, 159, 67, 0.45); +} + +/* 活动标签切换 - 与首页风格一致但使用橙色系 */ +.tab-section { + padding: 32rpx 0; + background: transparent; + margin: 0 32rpx 32rpx; +} + +.tab-scroll { + width: 100%; + white-space: nowrap; +} + +.tab-list { + display: inline-flex; + gap: 20rpx; + padding: 0 4rpx; +} + +.tab-item { + padding: 20rpx 48rpx; + border-radius: 100rpx; + font-size: 32rpx; + font-weight: 700; + color: #6A7282; + background: rgba(255, 255, 255, 0.6); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + flex-shrink: 0; +} + +.tab-item.active { + color: #fff; + background: linear-gradient(135deg, #FF9F43 0%, #FFBE76 100%); + box-shadow: 0 12rpx 24rpx rgba(255, 159, 67, 0.3); + transform: scale(1.02); +} + +/* 活动列表 */ +.activity-list { + padding: 0 32rpx; +} + +.activity-card { + background: #fff; + border-radius: 48rpx; + margin-bottom: 32rpx; + overflow: hidden; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.activity-image-wrap { + width: 100%; + height: 400rpx; + position: relative; + overflow: hidden; +} + +.activity-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.activity-image-gradient { + width: 100%; + height: 100%; + background: linear-gradient(135deg, #FFF4E6 0%, #FFF9F0 100%); +} + +.like-badge { + position: absolute; + top: 32rpx; + right: 32rpx; + display: flex; + align-items: center; + gap: 12rpx; + padding: 12rpx 28rpx 12rpx 16rpx; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(8px); + border-radius: 100rpx; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1); +} + +.like-icon { + width: 36rpx; + height: 36rpx; +} + +.like-count { + font-size: 32rpx; + font-weight: 700; + color: #4A5565; +} + +.price-tag { + position: absolute; + bottom: 32rpx; + left: 32rpx; + padding: 12rpx 28rpx; + border-radius: 100rpx; + font-size: 32rpx; + font-weight: 700; + color: #fff; +} + +.price-tag.paid { + background: linear-gradient(135deg, #F97316 0%, #EA580C 100%); +} + +.price-tag.free { + background: linear-gradient(135deg, #4ADE80 0%, #16A34A 100%); +} + +.activity-info { + padding: 40rpx; +} + +.activity-title { + font-size: 40rpx; + font-weight: 700; + color: #1A1A1A; + margin-bottom: 24rpx; + display: block; +} + +.activity-meta { + display: flex; + flex-direction: column; + gap: 20rpx; + margin-bottom: 24rpx; +} + +.meta-row { + display: flex; + align-items: center; + justify-content: space-between; +} + +.meta-item { + display: flex; + align-items: center; + gap: 16rpx; +} + +.meta-icon { + width: 36rpx; + height: 36rpx; + opacity: 0.6; +} + +.meta-text { + font-size: 30rpx; + color: #4A5565; +} + +.heat-text { + color: #FF9F43; + font-weight: 700; +} + +.activity-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 24rpx; + border-top: 2rpx solid #F3F4F6; +} + +.participants { + display: flex; + align-items: center; + gap: 16rpx; +} + +.avatar-stack { + display: flex; +} + +.mini-avatar { + width: 56rpx; + height: 56rpx; + border-radius: 50%; + background: #F3F4F6; + border: 2rpx solid #fff; + margin-left: -12rpx; +} + +.mini-avatar:first-child { + margin-left: 0; +} + +.participant-text { + font-size: 28rpx; + color: #6A7282; +} + +.signup-btn { + width: 220rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #FF9F43 0%, #FFBE76 100%); + border-radius: 100rpx; + font-size: 30rpx; + font-weight: 700; + color: #fff; + box-shadow: 0 8rpx 16rpx rgba(255, 159, 67, 0.3); + white-space: nowrap; + flex-shrink: 0; +} + +.signup-btn.signed { + background: #9CA3AF; + box-shadow: none; +} + +/* 空状态 */ +.empty-state { + padding: 100rpx 0; + text-align: center; +} + +.empty-icon { + width: 200rpx; + height: 200rpx; + opacity: 0.5; +} + +.empty-text { + font-size: 32rpx; + color: #FFBE76; +} + +/* 二维码弹窗 */ +.qrcode-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10000; + display: none; + align-items: center; + justify-content: center; +} + +.qrcode-modal.show { + display: flex; +} + +.modal-mask { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); +} + +.modal-content { + position: relative; + width: 600rpx; + background: #fff; + border-radius: 48rpx; + padding: 60rpx; + display: flex; + flex-direction: column; + align-items: center; +} + +.close-btn { + position: absolute; + top: 30rpx; + right: 30rpx; + width: 60rpx; + height: 60rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.close-icon { + width: 32rpx; + height: 32rpx; +} + +.modal-title { + font-size: 44rpx; + font-weight: 700; + margin-bottom: 12rpx; +} + +.modal-subtitle { + font-size: 28rpx; + color: #666; + margin-bottom: 40rpx; +} + +.qrcode-container { + width: 400rpx; + height: 400rpx; + border: 2rpx solid #eee; + border-radius: 24rpx; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 40rpx; +} + +.qrcode-image { + width: 360rpx; + height: 360rpx; +} + +.modal-tips { + font-size: 24rpx; + color: #999; + margin-bottom: 24rpx; +} + +.save-btn { + width: 100%; + height: 88rpx; + background: linear-gradient(135deg, #FF9F43 0%, #FFBE76 100%); + color: #fff; + border-radius: 44rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + font-weight: 700; +} diff --git a/pages/housekeeping-apply/housekeeping-apply.js b/pages/housekeeping-apply/housekeeping-apply.js new file mode 100644 index 0000000..ae4ce25 --- /dev/null +++ b/pages/housekeeping-apply/housekeeping-apply.js @@ -0,0 +1,260 @@ +// pages/housekeeping-apply/housekeeping-apply.js +// 家政保洁申请页面 +const api = require('../../utils/api') + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + showForm: true, + applyStatus: 'none', + statusTitle: '', + statusDesc: '', + isReapply: false, + agreed: false, + formData: { + avatar: '', + realName: '', + gender: '', + age: '', + idCard: '', + city: '', + serviceArea: '', + serviceTypes: [], + workYears: '', + healthCert: '', + idFront: '', + idBack: '', + skillCert: '', + introduction: '', + phone: '' + }, + canSubmit: false + }, + + onLoad(options) { + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight, + isReapply: options.isReapply === 'true' + }) + + this.checkApplyStatus() + }, + + goBack() { + wx.navigateBack() + }, + + async checkApplyStatus() { + const token = wx.getStorageSync('auth_token') + if (!token) { + this.setData({ applyStatus: 'none' }) + return + } + + try { + const res = await api.request('/housekeeping/apply') + if (res.success && res.data) { + const data = res.data + if (data.status === 'approved') { + this.setData({ + applyStatus: 'approved', + statusTitle: '申请已通过', + statusDesc: '恭喜您成为家政服务师!' + }) + } else if (data.status === 'pending') { + this.setData({ + applyStatus: 'pending', + statusTitle: '审核中', + statusDesc: '您的申请正在审核中,请耐心等待' + }) + } else if (data.status === 'rejected') { + this.setData({ + applyStatus: 'rejected', + statusTitle: '申请未通过', + statusDesc: data.rejectReason || '很抱歉,您的申请未通过审核' + }) + } + } + } catch (err) { + console.log('获取申请状态失败:', err) + this.setData({ applyStatus: 'none' }) + } + }, + + reapply() { + this.setData({ isReapply: true, applyStatus: 'none' }) + }, + + chooseAvatar() { + this.doChooseMedia('avatar') + }, + + uploadCert(e) { + const type = e.currentTarget.dataset.type + this.doChooseMedia(type) + }, + + doChooseMedia(field) { + wx.chooseMedia({ + count: 1, + mediaType: ['image'], + sourceType: ['album', 'camera'], + success: async (res) => { + const tempFilePath = res.tempFiles[0].tempFilePath + wx.showLoading({ title: '上传中...' }) + try { + const uploadRes = await api.uploadFile(tempFilePath, 'housekeeping') + if (uploadRes.success) { + const fieldMap = { + 'avatar': 'formData.avatar', + 'health': 'formData.healthCert', + 'idFront': 'formData.idFront', + 'idBack': 'formData.idBack', + 'skill': 'formData.skillCert' + } + this.setData({ [fieldMap[field]]: uploadRes.data.url }) + this.checkCanSubmit() + } + } catch (err) { + wx.showToast({ title: '上传失败', icon: 'none' }) + } finally { + wx.hideLoading() + } + } + }) + }, + + onInputChange(e) { + const field = e.currentTarget.dataset.field + this.setData({ [`formData.${field}`]: e.detail.value }) + this.checkCanSubmit() + }, + + selectGender(e) { + this.setData({ 'formData.gender': e.currentTarget.dataset.gender }) + this.checkCanSubmit() + }, + + toggleServiceType(e) { + const type = e.currentTarget.dataset.type + const serviceTypes = [...this.data.formData.serviceTypes] + const index = serviceTypes.indexOf(type) + if (index > -1) { + serviceTypes.splice(index, 1) + } else { + serviceTypes.push(type) + } + this.setData({ 'formData.serviceTypes': serviceTypes }) + this.checkCanSubmit() + }, + + toggleAgreement() { + this.setData({ agreed: !this.data.agreed }) + this.checkCanSubmit() + }, + + viewAgreement() { + wx.navigateTo({ url: '/pages/agreement/agreement?code=housekeeping_service' }) + }, + + checkCanSubmit() { + const { formData, agreed } = this.data + const idCardValid = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(formData.idCard) + + const canSubmit = + formData.realName && + formData.gender && + formData.age && + formData.idCard && idCardValid && + formData.city && + formData.serviceArea && + formData.serviceTypes.length > 0 && + formData.workYears && + formData.healthCert && + formData.idFront && + formData.idBack && + formData.introduction && + formData.introduction.length >= 50 && + formData.phone && + formData.phone.length === 11 && + agreed + + this.setData({ canSubmit }) + }, + + async submitApply() { + if (!this.data.canSubmit) return + + const { formData } = this.data + + if (!/^1[3-9]\d{9}$/.test(formData.phone)) { + wx.showToast({ title: '请输入正确的手机号', icon: 'none' }) + return + } + + const age = parseInt(formData.age) + if (age < 18 || age > 60) { + wx.showToast({ title: '年龄需在18-60岁之间', icon: 'none' }) + return + } + + wx.showLoading({ title: '提交中...' }) + try { + const res = await api.request('/housekeeping/apply', { + method: 'POST', + data: { + avatar: formData.avatar, + realName: formData.realName, + gender: formData.gender, + age: age, + idCard: formData.idCard, + city: formData.city, + serviceArea: formData.serviceArea, + serviceTypes: formData.serviceTypes, + workYears: parseInt(formData.workYears), + healthCert: formData.healthCert, + idFront: formData.idFront, + idBack: formData.idBack, + skillCert: formData.skillCert, + introduction: formData.introduction, + phone: formData.phone + } + }) + + if (res.success || res.code === 0) { + wx.showToast({ title: '申请已提交', icon: 'success' }) + this.setData({ + applyStatus: 'pending', + statusTitle: '审核中', + statusDesc: '您的申请正在审核中,请耐心等待', + isReapply: false + }) + } else { + wx.showToast({ title: res.message || '提交失败', icon: 'none' }) + } + } catch (err) { + if (err.code === 404) { + wx.showModal({ + title: '提示', + content: '家政保洁服务即将开放,敬请期待!', + showCancel: false, + confirmColor: '#b06ab3' + }) + } else { + wx.showToast({ title: err.message || '提交失败', icon: 'none' }) + } + } finally { + wx.hideLoading() + } + } +}) diff --git a/pages/housekeeping-apply/housekeeping-apply.json b/pages/housekeeping-apply/housekeeping-apply.json new file mode 100644 index 0000000..a3e86ce --- /dev/null +++ b/pages/housekeeping-apply/housekeeping-apply.json @@ -0,0 +1,5 @@ +{ + "usingComponents": {}, + "navigationStyle": "custom", + "navigationBarTextStyle": "black" +} diff --git a/pages/housekeeping-apply/housekeeping-apply.wxml b/pages/housekeeping-apply/housekeeping-apply.wxml new file mode 100644 index 0000000..391aa19 --- /dev/null +++ b/pages/housekeeping-apply/housekeeping-apply.wxml @@ -0,0 +1,267 @@ + + + + + + + + 返回 + + 家政保洁 + + + + + + + + + + + + + + + + {{statusTitle}} + {{statusDesc}} + + + + + + + + + 个人照片 + * + + + + + + + 上传照片 + + + + + + + + + 基本信息 + + + + + 姓名 + * + + + + + + + + + 性别 + * + + + + + + + + + + + + + + 年龄 + * + + + + + + + + + 身份证号 + * + + + + + + + + + + + 服务信息 + + + + + 服务城市 + * + + + + + + + + + 服务区域 + * + + + + + + + + + 服务项目 + * + + + 日常保洁 + 深度清洁 + 开荒保洁 + 家电清洗 + 收纳整理 + 擦玻璃 + + + + + + 从业年限 + * + + + + + + + + + + + 资质证书 + + + + + 健康证 + * + + + + + + 上传健康证 + + + + + + + 身份证正面 + * + + + + + + 上传身份证正面 + + + + + + + 身份证反面 + * + + + + + + 上传身份证反面 + + + + + + + 技能证书 + + + + + + 上传技能证书(选填) + + + + + + + + + 个人介绍 + * + + + + + + {{formData.introduction.length || 0}}/500 + + + + + + + 联系方式 + + + + + 手机号 + * + + + + + + + + + + + + + + 我已阅读并同意 + 《家政服务协议》 + + + + + + + + + + + diff --git a/pages/housekeeping-apply/housekeeping-apply.wxss b/pages/housekeeping-apply/housekeeping-apply.wxss new file mode 100644 index 0000000..4b324c0 --- /dev/null +++ b/pages/housekeeping-apply/housekeeping-apply.wxss @@ -0,0 +1,299 @@ +/* 家政保洁申请页面样式 */ +.page-container { + min-height: 100vh; + background: linear-gradient(180deg, #E8C3D4 0%, #F5E6EC 100%); +} + +.nav-header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + background: linear-gradient(135deg, #E8C3D4 0%, #D4A5C9 100%); +} + +.nav-content { + display: flex; + align-items: center; + justify-content: space-between; + height: 44px; + padding: 0 16px; +} + +.nav-back { + display: flex; + align-items: center; + min-width: 60px; +} + +.back-icon { width: 20px; height: 20px; } +.back-text { font-size: 14px; color: #333; margin-left: 4px; } +.nav-title { font-size: 17px; font-weight: 600; color: #333; } +.nav-placeholder { min-width: 60px; } + +.content-scroll { + height: 100vh; + padding-bottom: env(safe-area-inset-bottom); +} + +.intro-section { padding: 16px; } + +.banner-card { + height: 160px; + border-radius: 16px; + overflow: hidden; + margin-bottom: 16px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.banner-gradient { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.banner-title { font-size: 24px; font-weight: 600; color: #fff; margin-bottom: 8px; } +.banner-subtitle { font-size: 14px; color: rgba(255,255,255,0.9); } + +.info-card { + background: #fff; + border-radius: 16px; + padding: 20px; + margin-bottom: 16px; + box-shadow: 0 2px 8px rgba(0,0,0,0.05); +} + +.card-header { + display: flex; + align-items: center; + margin-bottom: 16px; +} + +.card-icon { + width: 32px; + height: 32px; + background: linear-gradient(135deg, #E8C3D4 0%, #D4A5C9 100%); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 12px; +} + +.card-icon image { width: 20px; height: 20px; } +.card-title { font-size: 18px; font-weight: 600; color: #333; } +.intro-text { font-size: 14px; color: #666; line-height: 1.8; } + +.service-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; +} + +.service-item { + background: #f8f8f8; + border-radius: 8px; + padding: 12px 8px; + text-align: center; +} + +.service-name { font-size: 13px; color: #333; } + +.advantage-list { padding: 0; } + +.advantage-item { + display: flex; + align-items: center; + padding: 8px 0; +} + +.advantage-dot { + width: 6px; + height: 6px; + background: linear-gradient(135deg, #b06ab3 0%, #4568dc 100%); + border-radius: 50%; + margin-right: 12px; +} + +.advantage-text { font-size: 14px; color: #666; } + +.apply-btn-area { padding: 24px 0; text-align: center; } + +.apply-btn { + width: 100%; + height: 48px; + background: linear-gradient(135deg, #b06ab3 0%, #4568dc 100%); + border-radius: 24px; + color: #fff; + font-size: 16px; + font-weight: 500; + border: none; + margin-bottom: 12px; +} + +.apply-tip { font-size: 13px; color: #999; } + +.apply-form { padding: 16px; } + +.status-card { + background: #fff; + border-radius: 16px; + padding: 40px 24px; + text-align: center; + margin-bottom: 16px; +} + +.status-icon { width: 80px; height: 80px; margin: 0 auto 16px; } +.status-icon image { width: 100%; height: 100%; } +.status-title { display: block; font-size: 18px; font-weight: 600; color: #333; margin-bottom: 8px; } +.status-desc { font-size: 14px; color: #666; line-height: 1.6; } + +.btn-secondary { + margin-top: 24px; + width: 160px; + height: 44px; + background: #fff; + border: 1px solid #b06ab3; + border-radius: 22px; + color: #b06ab3; + font-size: 15px; +} + +.form-content { + background: #fff; + border-radius: 16px; + padding: 24px 20px; +} + +.form-header { text-align: center; margin-bottom: 24px; } +.form-title { display: block; font-size: 20px; font-weight: 600; color: #333; margin-bottom: 8px; } +.form-subtitle { font-size: 14px; color: #999; } + +.form-section { margin-bottom: 24px; } +.section-header { display: flex; align-items: center; margin-bottom: 16px; } +.section-title { font-size: 16px; font-weight: 600; color: #333; } +.required { color: #ff4d4f; margin-left: 4px; } + +.avatar-upload-area { display: flex; justify-content: center; margin-bottom: 8px; } + +.avatar-circle { + width: 100px; + height: 100px; + border-radius: 50%; + background: #f5f5f5; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; +} + +.avatar-image { width: 100%; height: 100%; } +.upload-placeholder { text-align: center; } +.camera-icon { width: 32px; height: 32px; margin-bottom: 4px; } +.upload-text { font-size: 12px; color: #999; } + +.form-item { margin-bottom: 16px; } +.item-label-row { display: flex; align-items: center; margin-bottom: 8px; } +.item-label { font-size: 14px; color: #333; } +.input-wrapper { background: #f8f8f8; border-radius: 8px; padding: 0 12px; } +.item-input { width: 100%; height: 44px; font-size: 14px; color: #333; } + +.gender-options { display: flex; gap: 12px; } + +.gender-btn { + flex: 1; + height: 44px; + background: #f8f8f8; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + color: #666; +} + +.gender-btn.active { + background: linear-gradient(135deg, #E8C3D4 0%, #D4A5C9 100%); + color: #333; + font-weight: 500; +} + +.service-types { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.service-btn { + padding: 8px 16px; + background: #f8f8f8; + border-radius: 20px; + font-size: 13px; + color: #666; +} + +.service-btn.active { + background: linear-gradient(135deg, #E8C3D4 0%, #D4A5C9 100%); + color: #333; +} + +.cert-upload { + width: 100%; + height: 120px; + background: #f8f8f8; + border-radius: 8px; + border: 1px dashed #ddd; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.cert-image { width: 100%; height: 100%; } +.cert-placeholder { text-align: center; } +.upload-icon { width: 40px; height: 40px; margin-bottom: 8px; } + +.textarea-wrapper { background: #f8f8f8; border-radius: 8px; padding: 12px; } +.intro-textarea { width: 100%; height: 120px; font-size: 14px; color: #333; line-height: 1.6; } +.textarea-footer { display: flex; justify-content: flex-end; margin-top: 8px; } +.char-count { font-size: 12px; color: #999; } + +.agreement-row { display: flex; align-items: center; margin: 24px 0; } + +.checkbox { + width: 20px; + height: 20px; + border: 1px solid #ddd; + border-radius: 4px; + margin-right: 8px; + display: flex; + align-items: center; + justify-content: center; +} + +.checkbox.checked { + background: linear-gradient(135deg, #b06ab3 0%, #4568dc 100%); + border-color: transparent; +} + +.check-icon { width: 14px; height: 14px; } +.normal-text { font-size: 13px; color: #666; } +.link-text { font-size: 13px; color: #b06ab3; } + +.submit-btn { + width: 100%; + height: 48px; + background: linear-gradient(135deg, #b06ab3 0%, #4568dc 100%); + border-radius: 24px; + color: #fff; + font-size: 16px; + font-weight: 500; + border: none; +} + +.submit-btn.disabled { opacity: 0.5; } +.bottom-placeholder { height: 40px; } diff --git a/pages/index/index.js b/pages/index/index.js new file mode 100644 index 0000000..964ce91 --- /dev/null +++ b/pages/index/index.js @@ -0,0 +1,1586 @@ +// pages/index/index.js +// 首页 - 角色列表展示(卡片滑动) + +const app = getApp() +const api = require('../../utils/api') +const util = require('../../utils/util') +const config = require('../../config/index') +const proactiveMessage = require('../../utils/proactiveMessage') + +// 获取静态资源基础URL(去掉/api后缀) +const getStaticBaseUrl = () => { + const apiUrl = config.API_BASE_URL + return apiUrl.replace(/\/api$/, '') +} + +// 好友故事数据 - 将在加载角色后动态更新 +let STORY_USERS = [] + +Page({ + data: { + statusBarHeight: 44, + navHeight: 96, + + // 角色数据 + profiles: [], + currentIndex: 0, + loading: true, + error: null, + + // 故事用户 + storyUsers: STORY_USERS, + + // 首页 Banner + homeBanner: '', + bannerHeight: 240, + + // 交互状态 + likedProfiles: {}, + unlockedProfiles: {}, + showVipModal: false, + + // 爱心弹窗 + showHeartPopup: false, + currentCharacter: null, + heartCount: 0, + heartPackages: [ + { id: 1, hearts: 10, price: 6, tag: '' }, + { id: 2, hearts: 30, price: 18, tag: '热门' }, + { id: 3, hearts: 50, price: 28, tag: '' }, + { id: 4, hearts: 100, price: 50, tag: '超值' }, + { id: 5, hearts: 200, price: 98, tag: '' }, + { id: 6, hearts: 500, price: 238, tag: '最划算' } + ], + selectedHeartPackage: 1, // 默认选中第二个(热门) + purchasing: false, + + // 未读消息数 + totalUnread: 0, + + // 滑动相关 + startX: 0, + startY: 0, + offsetX: 0, + rotation: 0, + swipeDirection: '', + + // 分享配置 + shareConfig: null, + + // 注册奖励 + showRegistrationReward: false, + registrationRewardAmount: 0, + claiming: false, + auditStatus: 0, + + // GF100 弹窗 + showGf100Popup: false, + gf100ImageUrl: '', + + // 解锁配置 + unlockHeartsCost: 500 // 默认解锁爱心成本 + }, + + async onLoad() { + // 获取系统信息 + const { statusBarHeight, navHeight, auditStatus } = app.globalData + this.setData({ statusBarHeight, navHeight, auditStatus }) + + // 如果是审核状态,重定向到文娱页面 + if (auditStatus === 1) { + wx.switchTab({ + url: '/pages/entertainment/entertainment' + }) + return + } + + // 加载首页素材 (Banner等) + this.loadHomeAssets() + + // 加载分享配置 + this.loadShareConfig() + + // 加载角色列表 + this.loadCharacters() + + // 加载用户爱心余额 + this.loadHeartBalance() + + // 加载解锁配置 + this.loadUnlockConfig() + }, + + /** + * 加载解锁配置 + */ + async loadUnlockConfig() { + try { + // 获取当前列表中的第一个角色ID用于查询配置(配置是全局的,任一ID即可) + const charId = this.data.profiles[0]?.id || 'default' + const res = await api.chat.getQuota(charId) + + if (res.success && res.data && res.data.unlock_config) { + const cost = res.data.unlock_config.hearts_cost + if (typeof cost === 'number') { + this.setData({ + unlockHeartsCost: cost + }) + console.log('[index] 已从后端同步解锁成本:', cost) + } + } + } catch (err) { + console.log('[index] 加载解锁配置失败,使用默认值', err) + } + }, + + /** + * 加载分享配置 + */ + async loadShareConfig() { + try { + const res = await api.promotion.getShareConfig('index') + if (res.success && res.data) { + // 处理图片URL,确保是完整路径 + const shareConfig = { + ...res.data, + imageUrl: res.data.imageUrl ? util.getFullImageUrl(res.data.imageUrl) : '' + } + this.setData({ + shareConfig + }) + } + } catch (error) { + console.error('加载分享配置失败:', error) + } + }, + + /** + * 处理图片URL,如果是相对路径则拼接域名,并设置清晰度为85 + */ + processImageUrl(url) { + return util.getFullImageUrl(url) + }, + + /** + * 轮播图图片加载完成,自适应高度 + */ + onBannerLoad(e) { + const { width, height } = e.detail; + const sysInfo = wx.getSystemInfoSync(); + // 减去左右padding (32rpx * 2) + const swiperWidth = sysInfo.windowWidth - (32 * 2 / 750 * sysInfo.windowWidth); + const ratio = width / height; + const bannerHeight = swiperWidth / ratio; + const bannerHeightRpx = bannerHeight * (750 / sysInfo.windowWidth); + + this.setData({ + bannerHeight: bannerHeightRpx + }); + }, + + /** + * 加载首页素材 + */ + async loadHomeAssets() { + try { + const res = await api.pageAssets.getAssets('banners') + console.log('首页素材 API响应:', res) + + if (res.success && res.data) { + // 优先使用 home_banner,其次使用 companion_banner + const bannerUrl = res.data.home_banner || res.data.companion_banner + if (bannerUrl) { + this.setData({ + homeBanner: this.processImageUrl(bannerUrl) + }) + console.log('已加载首页Banner') + } + } + } catch (err) { + console.error('加载首页素材失败', err) + } + }, + + onShow() { + // 隐藏默认tabbar + wx.hideTabBar({ animation: false }) + + const app = getApp() + this.setData({ + auditStatus: app.globalData.auditStatus + }) + + // 如果是审核状态,重定向到文娱页面 + if (app.globalData.auditStatus === 1) { + wx.switchTab({ + url: '/pages/entertainment/entertainment' + }) + return + } + + // 刷新爱心余额 + this.loadHeartBalance() + + // 加载未读消息数 + this.loadUnreadCount() + + // 检查AI角色主动推送消息 + this.checkProactiveMessages() + + // 检查注册奖励 + this.checkRegistrationReward() + + // 检查 GF100 弹窗 + this.checkGf100Popup() + }, + + onPullDownRefresh() { + Promise.all([ + this.loadCharacters(), + this.loadHeartBalance() + ]).then(() => { + wx.stopPullDownRefresh() + }) + }, + + /** + * 加载用户爱心值 + * 使用 /api/auth/me 接口,该接口从 im_users.grass_balance 读取余额 + */ + async loadHeartBalance() { + try { + const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) + if (!token) { + this.setData({ heartCount: 0 }) + return + } + + // 使用 auth.getCurrentUser 接口,该接口返回 grass_balance 字段 + const res = await api.auth.getCurrentUser() + if (res.success && res.data) { + this.setData({ + heartCount: res.data.grass_balance || 0 + }) + console.log('[index] 爱心值加载成功:', res.data.grass_balance) + } + } catch (err) { + console.log('加载爱心值失败', err) + } + }, + + /** + * 加载未读消息数 + * 包含会话未读数 + 主动推送消息数 + */ + async loadUnreadCount() { + if (!app.globalData.isLoggedIn) { + this.setData({ totalUnread: 0 }) + return + } + + try { + // 并行获取会话列表和主动推送消息 + const [convRes, proactiveRes] = await Promise.all([ + api.chat.getConversations(), + api.proactiveMessage.getPending() + ]) + + let totalUnread = 0 + + // 计算会话未读数 + if (convRes.success && convRes.data) { + totalUnread = convRes.data.reduce((sum, conv) => sum + (conv.unread_count || 0), 0) + } + + // 加上主动推送消息数 + if (proactiveRes.success && proactiveRes.data && Array.isArray(proactiveRes.data)) { + totalUnread += proactiveRes.data.length + console.log('[index] 主动推送消息数:', proactiveRes.data.length) + } + + this.setData({ totalUnread }) + console.log('[index] 总未读消息数:', totalUnread) + } catch (err) { + console.log('获取未读消息数失败', err) + this.setData({ totalUnread: 0 }) + } + }, + + /** + * 检查AI角色主动推送消息 + * 注意:未读数已在 loadUnreadCount 中计算,不在首页显示Toast提示 + */ + async checkProactiveMessages() { + if (!app.globalData.isLoggedIn) { + return + } + + try { + const messages = await proactiveMessage.checkAndShowMessages({ + force: true // 首次进入强制检查 + }) + + console.log('[index] 主动推送消息检查完成,消息数:', messages.length) + } catch (err) { + console.log('[index] 检查主动推送消息失败', err) + } + }, + + /** + * 加载角色列表 + */ + async loadCharacters() { + this.setData({ loading: true, error: null }) + + try { + console.log('[index] 开始加载角色列表...') + const res = await api.character.getRandom(10) + console.log('[index] API响应:', JSON.stringify(res)) + + // 兼容两种返回格式: + // 1. { code: 0, data: { list: [...] } } - 新格式 + // 2. { success: true, data: [...] } - 旧格式 + let characters = [] + if (res.code === 0 && res.data) { + characters = res.data.list || res.data + if (!Array.isArray(characters)) { + characters = [] + } + } else if (res.success && res.data) { + characters = Array.isArray(res.data) ? res.data : (res.data.list || []) + } + + console.log('[index] 解析到角色数量:', characters.length) + + if (characters.length > 0) { + // 转换数据格式:后端格式 -> 前端格式 + const profiles = characters.map(char => this.transformCharacter(char)) + + // 更新好友故事区域(取前8个角色) + const storyUsers = profiles.slice(0, 8).map(p => ({ + id: p.id, + name: p.name, + img: p.avatar + })) + + this.setData({ + profiles, + storyUsers, + currentIndex: 0, + loading: false + }) + + console.log('[index] 角色加载成功,数量:', profiles.length) + } else { + console.log('[index] 没有角色数据') + this.setData({ + loading: false, + profiles: [], + currentIndex: 0, + error: '暂无角色数据' + }) + } + } catch (err) { + console.error('[index] 加载角色失败', err) + this.setData({ + loading: false, + profiles: [], + currentIndex: 0, + error: err.message || '加载失败' + }) + // 不显示toast,让用户看到空状态页面 + } + }, + + /** + * 转换角色数据格式 + * @param {object} char - 后端角色数据 + */ + transformCharacter(char) { + // 静态资源基础URL + const staticBaseUrl = getStaticBaseUrl() + + // 转换照片路径为完整URL + const convertPhotoUrl = (url) => { + if (!url) return '' + // 如果已经是完整URL,直接返回 + if (url.startsWith('http://') || url.startsWith('https://')) { + return url + } + // 如果是相对路径,拼接基础URL + if (url.startsWith('/characters/')) { + return staticBaseUrl + url + } + return url + } + + // 头像URL(用于推荐栏圆形小头像)- 优先使用 avatar/logo + const avatarUrl = convertPhotoUrl(char.avatar || char.logo || char.image) + + // 宣传图URL(用于卡片大图)- 优先使用 promoImage + const promoImageUrl = convertPhotoUrl(char.promoImage || char.promo_image || char.avatar || char.image) + + return { + id: char.id, + name: char.name || '未知', + height: char.height || '', + location: char.location || char.province || '', + occupation: char.occupation || '', + hobbies: char.hobbies || '', + tags: char.tags || [], + bio: char.bio || char.description || char.self_introduction || '', + image: promoImageUrl, // 卡片大图使用宣传图 + avatar: avatarUrl, // 小头像使用头像 + gender: char.gender || 'female', + age: char.age || '', + voiceId: char.voice_id, + isVipOnly: char.is_vip_only || false, + // 开场白音频URL + greetingAudioUrl: char.greetingAudioUrl || char.greeting_audio_url || '' + } + }, + + // ==================== 滑动交互 ==================== + + onTouchStart(e) { + this.setData({ + startX: e.touches[0].clientX, + startY: e.touches[0].clientY, + swipeDirection: '' + }) + }, + + onTouchMove(e) { + const moveX = e.touches[0].clientX - this.data.startX + const rotation = moveX * 0.05 + + this.setData({ + offsetX: moveX, + rotation: Math.max(-15, Math.min(15, rotation)) + }) + }, + + onTouchEnd(e) { + const threshold = 80 + const { offsetX } = this.data + + if (offsetX > threshold) { + // 右滑 - 喜欢 + this.handleSwipeLike() + } else if (offsetX < -threshold) { + // 左滑 - 跳过 + this.handlePass() + } else { + // 重置位置 + this.setData({ + offsetX: 0, + rotation: 0 + }) + } + }, + + /** + * 处理喜欢操作 + */ + async handleSwipeLike() { + const { currentIndex, profiles, likedProfiles } = this.data + + if (currentIndex >= profiles.length) return + + const currentProfile = profiles[currentIndex] + const currentId = currentProfile.id + + // 更新本地状态 + const newLiked = { ...likedProfiles } + newLiked[currentId] = true + + this.setData({ + swipeDirection: 'swipe-right', + likedProfiles: newLiked + }) + + // 调用API(静默操作,不显示提示) + try { + await api.character.toggleLike(currentId) + } catch (err) { + console.log('喜欢操作失败', err) + } + + // 移动到下一张 + setTimeout(() => { + this.moveToNext() + }, 300) + }, + + /** + * 处理跳过操作 + */ + handlePass() { + this.setData({ swipeDirection: 'swipe-left' }) + + setTimeout(() => { + this.moveToNext() + }, 300) + }, + + /** + * 移动到下一张卡片 + */ + moveToNext() { + const { currentIndex, profiles } = this.data + const nextIndex = currentIndex + 1 + + // 如果快到末尾,加载更多 + if (nextIndex >= profiles.length - 2) { + this.loadMoreCharacters() + } + + this.setData({ + currentIndex: nextIndex, + offsetX: 0, + rotation: 0, + swipeDirection: '' + }) + }, + + /** + * 加载更多角色 + * 传递已显示的角色ID,避免重复 + */ + async loadMoreCharacters() { + try { + // 获取已显示的角色ID列表 + const existingIds = this.data.profiles.map(p => p.id).filter(id => id) + + const res = await api.character.getRandom(6, { excludeIds: existingIds }) + + // 兼容两种返回格式 + let characters = [] + if (res.code === 0 && res.data) { + characters = res.data.list || res.data + } else if (res.success && res.data) { + characters = Array.isArray(res.data) ? res.data : (res.data.list || []) + } + + if (characters.length > 0) { + // 再次过滤,确保不重复 + const existingIdSet = new Set(existingIds) + const filteredCharacters = characters.filter(char => !existingIdSet.has(char.id)) + + if (filteredCharacters.length > 0) { + const newProfiles = filteredCharacters.map(char => this.transformCharacter(char)) + + this.setData({ + profiles: [...this.data.profiles, ...newProfiles] + }) + } + } + } catch (err) { + console.log('加载更多失败', err) + } + }, + + // ==================== 按钮操作 ==================== + + /** + * 切换喜欢状态 - 显示爱心套餐弹窗 + */ + onToggleLike() { + const { currentIndex, profiles } = this.data + + if (currentIndex >= profiles.length) return + + const currentProfile = profiles[currentIndex] + + // 保存当前角色信息并显示弹窗 + this.setData({ + showHeartPopup: true, + currentCharacter: currentProfile + }) + }, + + /** + * 播放语音(优先使用预录制的开场白音频) + */ + async onPlayVoice() { + const { currentIndex, profiles } = this.data + + if (currentIndex >= profiles.length) return + + const currentProfile = profiles[currentIndex] + + // 优先使用预录制的开场白音频 + const greetingAudioUrl = currentProfile.greetingAudioUrl || currentProfile.greeting_audio_url + + if (greetingAudioUrl) { + // 处理相对路径,拼接完整URL + let audioUrl = greetingAudioUrl + if (audioUrl.startsWith('/')) { + audioUrl = getStaticBaseUrl() + audioUrl + } + + console.log('[index] 播放开场白音频:', audioUrl) + + // 停止之前的音频 + if (this.audioContext) { + try { + this.audioContext.stop() + this.audioContext.destroy() + } catch (e) {} + this.audioContext = null + } + + // 先检查音频文件是否存在 + wx.request({ + url: audioUrl, + method: 'HEAD', + success: (res) => { + if (res.statusCode === 200) { + // 文件存在,播放音频 + this.playAudioFile(audioUrl) + } else { + console.log('[index] 音频文件不存在:', res.statusCode) + wx.showToast({ title: '该角色暂无独白音频', icon: 'none' }) + } + }, + fail: (err) => { + console.log('[index] 检查音频文件失败:', err) + // 即使HEAD请求失败,也尝试播放(有些服务器不支持HEAD) + this.playAudioFile(audioUrl) + } + }) + return + } + + // 没有预录制音频,提示用户 + wx.showToast({ + title: '该角色暂无独白音频', + icon: 'none', + duration: 2000 + }) + }, + + /** + * 播放音频文件 + */ + playAudioFile(audioUrl) { + // 显示播放中提示 + wx.showToast({ + title: '播放独白中...', + icon: 'none', + duration: 5000 + }) + + // 创建音频上下文 + const innerAudioContext = wx.createInnerAudioContext() + this.audioContext = innerAudioContext + + // 配置音频属性 + innerAudioContext.src = audioUrl + innerAudioContext.volume = 1.0 + innerAudioContext.obeyMuteSwitch = false // 不受系统静音开关影响 + innerAudioContext.autoplay = false + + // 确保在 iOS 上即使在静音模式下也能播放(双重保险) + if (wx.setInnerAudioOption) { + wx.setInnerAudioOption({ + obeyMuteSwitch: false, + speakerOn: true + }) + } + + innerAudioContext.onCanplay(() => { + console.log('[index] 音频可以播放, duration:', innerAudioContext.duration) + }) + + innerAudioContext.onPlay(() => { + console.log('[index] 音频开始播放, volume:', innerAudioContext.volume) + }) + + innerAudioContext.onTimeUpdate(() => { + // 每秒打印一次进度 + const currentTime = Math.floor(innerAudioContext.currentTime) + if (currentTime !== this._lastLogTime) { + console.log('[index] 播放进度:', currentTime, '/', Math.floor(innerAudioContext.duration || 0)) + this._lastLogTime = currentTime + } + }) + + innerAudioContext.onError((err) => { + console.error('[index] 音频播放错误:', JSON.stringify(err)) + wx.hideToast() + let errMsg = '播放失败' + if (err.errCode === 10001 || err.errCode === -1) { + errMsg = '音频文件不存在' + } else if (err.errCode === 10002) { + errMsg = '网络错误' + } else if (err.errCode === 10003 || err.errCode === 10004) { + errMsg = '音频格式不支持' + } + wx.showToast({ title: errMsg, icon: 'none' }) + }) + + innerAudioContext.onEnded(() => { + console.log('[index] 音频播放结束') + wx.hideToast() + }) + + // 延迟播放 + setTimeout(() => { + console.log('[index] 调用 play()') + innerAudioContext.play() + }, 100) + }, + + /** + * 选择角色(进入详情) + */ + onSelectCharacter() { + const { currentIndex, profiles } = this.data + + if (currentIndex >= profiles.length) return + + const profile = profiles[currentIndex] + + wx.navigateTo({ + url: `/pages/character-detail/character-detail?id=${profile.id}` + }) + }, + + /** + * 点击卡片进入详情 + */ + onCardTap() { + // 如果有滑动偏移,不触发点击 + if (Math.abs(this.data.offsetX) > 10) { + return + } + + this.onSelectCharacter() + }, + + /** + * 刷新角色列表 + */ + onRefresh() { + this.loadCharacters() + + wx.showToast({ + title: '已为您换一批', + icon: 'success' + }) + }, + + // ==================== VIP弹窗 ==================== + + closeVipModal() { + this.setData({ showVipModal: false }) + }, + + preventBubble() { + // 阻止事件冒泡 + }, + + preventTouchMove() { + // 阻止滚动穿透 + }, + + // ==================== 爱心弹窗 ==================== + + /** + * 显示爱心弹窗 + */ + showHeartPopup() { + this.setData({ showHeartPopup: true }) + }, + + /** + * 关闭爱心弹窗 + */ + closeHeartPopup() { + this.setData({ + showHeartPopup: false, + currentCharacter: null + }) + }, + + /** + * 选择爱心套餐 + */ + selectHeartPackage(e) { + const index = e.currentTarget.dataset.index + this.setData({ selectedHeartPackage: index }) + }, + + /** + * 购买爱心 + * 使用 /api/payment/unified-order 接口 + * 测试模式下返回 testMode: true,订单直接完成,无需调用微信支付 + */ + async buyHearts() { + const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) + if (!token) { + wx.showToast({ title: '请先登录', icon: 'none' }) + setTimeout(() => { + wx.navigateTo({ url: '/pages/login/login' }) + }, 1500) + return + } + + const selectedPackage = this.data.heartPackages[this.data.selectedHeartPackage] + if (!selectedPackage) { + wx.showToast({ title: '请选择套餐', icon: 'none' }) + return + } + + this.setData({ purchasing: true }) + + try { + wx.showLoading({ title: '创建订单中...' }) + + // 调用统一支付订单接口 + const res = await api.payment.createUnifiedOrder({ + type: 'recharge', + amount: selectedPackage.price, + rechargeValue: selectedPackage.hearts + }) + + wx.hideLoading() + + console.log('[buyHearts] API返回:', JSON.stringify(res)) + + if (res.success) { + // 检查是否为测试模式(兼容多种判断方式) + const isTestMode = res.testMode || res.test_mode || res.data?.testMode || res.data?.test_mode || + // 如果 payParams.package 包含 mock_prepay,也认为是测试模式 + (res.payParams?.package && res.payParams.package.includes('mock_prepay')) + + if (isTestMode) { + // 测试模式:订单已直接完成,无需调用微信支付 + wx.showToast({ title: '购买成功', icon: 'success' }) + await this.loadHeartBalance() + this.closeHeartPopup() + } else { + // 正式模式:调用微信支付 + const payParams = res.payParams || res.pay_params || res.data?.payParams || res.data?.pay_params + + if (!payParams || !payParams.timeStamp) { + wx.showToast({ title: '支付参数错误', icon: 'none' }) + return + } + + wx.requestPayment({ + timeStamp: payParams.timeStamp, + nonceStr: payParams.nonceStr, + package: payParams.package, + signType: payParams.signType || 'RSA', + paySign: payParams.paySign, + success: async () => { + wx.showToast({ title: '购买成功', icon: 'success' }) + await this.loadHeartBalance() + this.closeHeartPopup() + }, + fail: (err) => { + if (err.errMsg !== 'requestPayment:fail cancel') { + wx.showToast({ title: '支付失败', icon: 'none' }) + } + } + }) + } + } else { + wx.showToast({ title: res.message || '创建订单失败', icon: 'none' }) + } + } catch (err) { + wx.hideLoading() + console.error('购买爱心失败', err) + wx.showToast({ title: '网络错误,请重试', icon: 'none' }) + } finally { + this.setData({ purchasing: false }) + } + }, + + /** + * 分享解锁 + */ + onShareUnlock() { + wx.showToast({ + title: '分享功能开发中', + icon: 'none' + }) + // TODO: 实现分享解锁逻辑 + }, + + /** + * 爱心兑换解锁 + */ + async onExchangeHearts() { + const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) + if (!token) { + wx.showToast({ title: '请先登录', icon: 'none' }) + setTimeout(() => { + wx.navigateTo({ url: '/pages/login/login' }) + }, 1500) + return + } + + const { currentCharacter, heartCount, unlockHeartsCost } = this.data + if (!currentCharacter) return + + // 检查爱心值,不足时提示并跳转充值页面 + if (heartCount < unlockHeartsCost) { + wx.showToast({ title: '爱心值不足,去充值', icon: 'none' }) + setTimeout(() => { + this.setData({ showHeartPopup: false }) + wx.navigateTo({ url: '/pages/recharge/recharge' }) + }, 1500) + return + } + + this.setData({ purchasing: true }) + + try { + wx.showLoading({ title: '兑换中...' }) + + const res = await api.character.unlock({ + character_id: currentCharacter.id, + unlock_type: 'hearts' + }) + + wx.hideLoading() + + if (res.success || res.code === 0) { + wx.showToast({ title: '解锁成功', icon: 'success' }) + + // 更新爱心余额(优先使用后端返回的余额,否则本地扣减) + const newBalance = res.data?.remaining_hearts ?? (this.data.heartCount - unlockHeartsCost) + this.setData({ + heartCount: newBalance + }) + + // 记录已解锁 + const newUnlocked = { ...this.data.unlockedProfiles } + newUnlocked[currentCharacter.id] = true + this.setData({ + unlockedProfiles: newUnlocked, + showHeartPopup: false, + currentCharacter: null + }) + + // 延迟后跳转到聊天页面 + setTimeout(() => { + wx.navigateTo({ + url: `/pages/chat-detail/chat-detail?id=${currentCharacter.id}&name=${encodeURIComponent(currentCharacter.name)}` + }) + }, 1000) + } else { + wx.showToast({ title: res.message || '兑换失败', icon: 'none' }) + } + } catch (err) { + wx.hideLoading() + console.error('爱心兑换失败', err) + wx.showToast({ title: '网络错误,请重试', icon: 'none' }) + } finally { + this.setData({ purchasing: false }) + } + }, + + /** + * 直接购买解锁(9.9元) + * 使用 /api/payment/unified-order 接口 + * 测试模式下返回 testMode: true,订单直接完成,无需调用微信支付 + */ + async onPurchaseDirect() { + const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) + if (!token) { + wx.showToast({ title: '请先登录', icon: 'none' }) + setTimeout(() => { + wx.navigateTo({ url: '/pages/login/login' }) + }, 1500) + return + } + + const { currentCharacter } = this.data + if (!currentCharacter) return + + this.setData({ purchasing: true }) + + try { + wx.showLoading({ title: '创建订单中...' }) + + // 调用统一支付订单接口 + const res = await api.payment.createUnifiedOrder({ + type: 'character_unlock', + character_id: currentCharacter.id, + amount: 9.9 + }) + + wx.hideLoading() + + console.log('[onPurchaseDirect] API返回:', JSON.stringify(res)) + + if (res.success) { + // 检查是否为测试模式(兼容多种判断方式) + const isTestMode = res.testMode || res.test_mode || res.data?.testMode || res.data?.test_mode || + // 如果 payParams.package 包含 mock_prepay,也认为是测试模式 + (res.payParams?.package && res.payParams.package.includes('mock_prepay')) + + if (isTestMode) { + // 测试模式:订单已直接完成,无需调用微信支付 + wx.showToast({ title: '购买成功', icon: 'success' }) + + // 记录已解锁 + const newUnlocked = { ...this.data.unlockedProfiles } + newUnlocked[currentCharacter.id] = true + this.setData({ + unlockedProfiles: newUnlocked, + showHeartPopup: false, + currentCharacter: null + }) + + // 延迟后跳转到聊天页面 + setTimeout(() => { + wx.navigateTo({ + url: `/pages/chat-detail/chat-detail?id=${currentCharacter.id}&name=${encodeURIComponent(currentCharacter.name)}` + }) + }, 1000) + } else { + // 正式模式:调用微信支付 + const payParams = res.payParams || res.pay_params || res.data?.payParams || res.data?.pay_params + + if (!payParams || !payParams.timeStamp) { + wx.showToast({ title: '支付参数错误', icon: 'none' }) + return + } + + wx.requestPayment({ + timeStamp: payParams.timeStamp, + nonceStr: payParams.nonceStr, + package: payParams.package, + signType: payParams.signType || 'RSA', + paySign: payParams.paySign, + success: () => { + wx.showToast({ title: '购买成功', icon: 'success' }) + + // 记录已解锁 + const newUnlocked = { ...this.data.unlockedProfiles } + newUnlocked[currentCharacter.id] = true + this.setData({ + unlockedProfiles: newUnlocked, + showHeartPopup: false, + currentCharacter: null + }) + + // 延迟后跳转到聊天页面 + setTimeout(() => { + wx.navigateTo({ + url: `/pages/chat-detail/chat-detail?id=${currentCharacter.id}&name=${encodeURIComponent(currentCharacter.name)}` + }) + }, 1000) + }, + fail: (err) => { + if (err.errMsg !== 'requestPayment:fail cancel') { + wx.showToast({ title: '支付失败', icon: 'none' }) + } + } + }) + } + } else { + wx.showToast({ title: res.message || '创建订单失败', icon: 'none' }) + } + } catch (err) { + wx.hideLoading() + console.error('购买解锁失败', err) + wx.showToast({ title: '网络错误,请重试', icon: 'none' }) + } finally { + this.setData({ purchasing: false }) + } + }, + + /** + * 购买并解锁角色聊天(套餐购买) + * 使用 /api/payment/unified-order 接口 + * 测试模式下返回 testMode: true,订单直接完成,无需调用微信支付 + */ + async buyAndUnlock() { + const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) + if (!token) { + wx.showToast({ title: '请先登录', icon: 'none' }) + setTimeout(() => { + wx.navigateTo({ url: '/pages/login/login' }) + }, 1500) + return + } + + const { currentCharacter, heartPackages, selectedHeartPackage } = this.data + if (!currentCharacter) return + + const selectedPackage = heartPackages[selectedHeartPackage] + + if (!selectedPackage) { + wx.showToast({ title: '请选择套餐', icon: 'none' }) + return + } + + this.setData({ purchasing: true }) + + try { + wx.showLoading({ title: '创建订单中...' }) + + // 调用统一支付订单接口 + const res = await api.payment.createUnifiedOrder({ + type: 'character_unlock', + character_id: currentCharacter.id, + amount: selectedPackage.price + }) + + wx.hideLoading() + + console.log('[buyAndUnlock] API返回:', JSON.stringify(res)) + + if (res.success) { + // 检查是否为测试模式(兼容多种判断方式) + const isTestMode = res.testMode || res.test_mode || res.data?.testMode || res.data?.test_mode || + // 如果 payParams.package 包含 mock_prepay,也认为是测试模式 + (res.payParams?.package && res.payParams.package.includes('mock_prepay')) + + if (isTestMode) { + // 测试模式:订单已直接完成,无需调用微信支付 + wx.showToast({ title: '购买成功', icon: 'success' }) + + // 刷新爱心余额 + this.loadHeartBalance() + + // 记录已解锁 + const newUnlocked = { ...this.data.unlockedProfiles } + newUnlocked[currentCharacter.id] = true + this.setData({ + unlockedProfiles: newUnlocked, + showHeartPopup: false, + currentCharacter: null + }) + + // 延迟后跳转到聊天页面 + setTimeout(() => { + wx.navigateTo({ + url: `/pages/chat-detail/chat-detail?id=${currentCharacter.id}&name=${encodeURIComponent(currentCharacter.name)}` + }) + }, 1000) + } else { + // 正式模式:调用微信支付 + const payParams = res.payParams || res.pay_params || res.data?.payParams || res.data?.pay_params + + if (!payParams || !payParams.timeStamp) { + wx.showToast({ title: '支付参数错误', icon: 'none' }) + return + } + + wx.requestPayment({ + timeStamp: payParams.timeStamp, + nonceStr: payParams.nonceStr, + package: payParams.package, + signType: payParams.signType || 'RSA', + paySign: payParams.paySign, + success: async () => { + wx.showToast({ title: '购买成功', icon: 'success' }) + + // 刷新爱心余额 + this.loadHeartBalance() + + // 记录已解锁 + const newUnlocked = { ...this.data.unlockedProfiles } + newUnlocked[currentCharacter.id] = true + this.setData({ + unlockedProfiles: newUnlocked, + showHeartPopup: false, + currentCharacter: null + }) + + // 延迟后跳转到聊天页面 + setTimeout(() => { + wx.navigateTo({ + url: `/pages/chat-detail/chat-detail?id=${currentCharacter.id}&name=${encodeURIComponent(currentCharacter.name)}` + }) + }, 1000) + }, + fail: (err) => { + if (err.errMsg !== 'requestPayment:fail cancel') { + wx.showToast({ title: '支付失败', icon: 'none' }) + } + } + }) + } + } else { + wx.showToast({ title: res.message || '创建订单失败', icon: 'none' }) + } + } catch (err) { + wx.hideLoading() + console.error('解锁角色失败', err) + wx.showToast({ title: '网络错误,请重试', icon: 'none' }) + } finally { + this.setData({ purchasing: false }) + } + }, + + /** + * 跳转到用户协议 + */ + goToUserAgreement() { + wx.navigateTo({ url: '/pages/agreement/agreement?code=user-agreement' }) + }, + + /** + * 跳转到隐私政策 + */ + goToPrivacyPolicy() { + wx.navigateTo({ url: '/pages/agreement/agreement?code=privacy-policy' }) + }, + + onUpgrade() { + const { currentIndex, profiles, unlockedProfiles, heartCount, unlockHeartsCost } = this.data + + if (currentIndex >= profiles.length) return + + // 检查爱心值,不足时提示并跳转充值页面 + if (heartCount < unlockHeartsCost) { + wx.showToast({ title: '爱心值不足,去充值', icon: 'none' }) + setTimeout(() => { + this.setData({ showVipModal: false }) + wx.navigateTo({ url: '/pages/recharge/recharge' }) + }, 1500) + return + } + + const currentId = profiles[currentIndex].id + + const newUnlocked = { ...unlockedProfiles } + newUnlocked[currentId] = true + + // 扣减爱心值 + this.setData({ + unlockedProfiles: newUnlocked, + showVipModal: false, + heartCount: heartCount - unlockHeartsCost + }) + + wx.showToast({ + title: '兑换成功!', + icon: 'success' + }) + }, + + onPurchase() { + // 跳转到充值页面 + wx.navigateTo({ + url: '/pages/recharge/recharge' + }) + }, + + // ==================== 其他操作 ==================== + + onStoryTap(e) { + const index = e.currentTarget.dataset.index + const storyUser = this.data.storyUsers[index] + if (storyUser && storyUser.id) { + // 跳转到角色详情页 + wx.navigateTo({ + url: `/pages/character-detail/character-detail?id=${storyUser.id}` + }) + } else { + wx.showToast({ + title: storyUser.name + '的故事', + icon: 'none' + }) + } + }, + + onNotification() { + wx.showToast({ + title: '暂无新消息', + icon: 'none' + }) + }, + + // Tab bar navigation - 需要登录的页面检查登录状态 + switchTab(e) { + const path = e.currentTarget.dataset.path + const app = getApp() + + // 消息页面需要登录 + if (path === '/pages/chat/chat') { + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ + url: '/pages/login/login?redirect=' + encodeURIComponent(path) + }) + return + } + } + wx.switchTab({ url: path }) + }, + + onTest() { + wx.showToast({ + title: '测试功能', + icon: 'none' + }) + }, + + /** + * 检查注册奖励领取资格 + */ + async checkRegistrationReward() { + if (!app.globalData.isLoggedIn) return + + try { + const res = await api.lovePoints.checkRegistrationReward() + console.log('[index] 注册奖励检查结果:', res) + if (res.success && res.data && res.data.eligible) { + this.setData({ + showRegistrationReward: true, + registrationRewardAmount: res.data.amount || 0 + }) + } + } catch (err) { + console.error('[index] 检查注册奖励失败:', err) + } + }, + + /** + * 领取注册奖励 + */ + async onClaimReward() { + if (this.data.claiming) return + + this.setData({ claiming: true }) + wx.showLoading({ title: '领取中...' }) + + try { + const res = await api.lovePoints.claimRegistrationReward() + wx.hideLoading() + + if (res.success) { + wx.showToast({ + title: '领取成功', + icon: 'success', + duration: 2000 + }) + + this.setData({ + showRegistrationReward: false + }) + + // 如果后端返回了免费畅聊时间,可以做提示 + if (res.data && res.data.free_chat_time) { + console.log('[index] 获得免费畅聊时间:', res.data.free_chat_time); + setTimeout(() => { + wx.showModal({ + title: '领取成功', + content: '恭喜获得 100 爱心 + 60 分钟免费畅聊时间!', + confirmText: '去聊天', + success: (modalRes) => { + if (modalRes.confirm) { + // 如果有当前正在查看的角色,直接跳过去 + const { currentIndex, profiles } = this.data + if (profiles && profiles[currentIndex]) { + const char = profiles[currentIndex] + wx.navigateTo({ + url: `/pages/chat-detail/chat-detail?id=${char.id}&name=${encodeURIComponent(char.name)}` + }) + } else { + wx.switchTab({ url: '/pages/chat/chat' }) + } + } + } + }); + }, 2000); + } + + // 刷新余额 + this.loadHeartBalance() + } else { + wx.showToast({ + title: res.message || '领取失败', + icon: 'none' + }) + } + } catch (err) { + wx.hideLoading() + console.error('[index] 领取注册奖励失败:', err) + wx.showToast({ + title: '网络错误,请重试', + icon: 'none' + }) + } finally { + this.setData({ claiming: false }) + } + }, + + /** + * 关闭注册奖励弹窗 + */ + closeRewardPopup() { + this.setData({ + showRegistrationReward: false + }) + }, + + /** + * 检查 GF100 弹窗状态 + */ + async checkGf100Popup() { + if (!app.globalData.isLoggedIn) return + + try { + const res = await api.lovePoints.checkGf100Status() + console.log('[index] GF100 检查结果:', res) + if (res.success && res.data && res.data.showPopup) { + const imageUrl = res.data.imageUrl; + this.setData({ + showGf100Popup: true, + gf100ImageUrl: imageUrl ? util.getFullImageUrl(imageUrl) : '/images/gf100.png' + }) + } + } catch (err) { + console.error('[index] 检查 GF100 弹窗失败:', err) + } + }, + + /** + * 领取 GF100 奖励 + */ + async onClaimGf100() { + if (this.data.claiming) return + + this.setData({ claiming: true }) + wx.showLoading({ title: '领取中...', mask: true }) + + try { + const res = await api.lovePoints.claimGf100() + wx.hideLoading() + + if (res.success) { + wx.showToast({ + title: '领取成功', + icon: 'success', + duration: 2000 + }) + + this.setData({ + showGf100Popup: false + }) + + // 弹出获得 60 分钟免费畅聊的提示 + setTimeout(() => { + wx.showModal({ + title: '领取成功', + content: '恭喜获得 100 爱心 + 60 分钟免费畅聊时间!', + confirmText: '去聊天', + success: (modalRes) => { + if (modalRes.confirm) { + wx.switchTab({ url: '/pages/chat/chat' }) + } + } + }); + }, 500); + + // 刷新余额 + this.loadHeartBalance() + } else { + wx.showToast({ + title: res.message || '领取失败', + icon: 'none' + }) + } + } catch (err) { + wx.hideLoading() + console.error('[index] 领取 GF100 失败:', err) + wx.showToast({ + title: '网络错误,请重试', + icon: 'none' + }) + } finally { + this.setData({ claiming: false }) + } + }, + + /** + * 关闭 GF100 弹窗 + */ + closeGf100Popup() { + this.setData({ + showGf100Popup: false + }) + }, + + /** + * 用户点击右上角分享 + */ + onShareAppMessage() { + const { shareConfig } = this.data + const referralCode = wx.getStorageSync('referralCode') || '' + + api.promotion.recordShare({ + type: 'app_message', + page: '/pages/index/index', + referralCode: referralCode + }).catch(err => console.error('记录分享失败:', err)) + + this.recordShareReward() + + if (shareConfig) { + return { + title: shareConfig.title, + path: `${shareConfig.path}?referralCode=${referralCode}`, + imageUrl: shareConfig.imageUrl + } + } + + return { + title: '欢迎来到心伴俱乐部', + desc: '随时可聊 一直陪伴', + path: `/pages/index/index?referralCode=${referralCode}`, + imageUrl: '/images/icon-heart-new.png' + } + }, + + /** + * 用户分享到朋友圈 + */ + onShareTimeline() { + const { shareConfig } = this.data + const referralCode = wx.getStorageSync('referralCode') || '' + + api.promotion.recordShare({ + type: 'timeline', + page: '/pages/index/index', + referralCode: referralCode + }).catch(err => console.error('记录分享失败:', err)) + + this.recordShareReward() + + if (shareConfig) { + return { + title: shareConfig.title, + query: `referralCode=${referralCode}`, + imageUrl: shareConfig.imageUrl + } + } + + return { + title: '心伴俱乐部 - 随时可聊 一直陪伴', + query: `referralCode=${referralCode}`, + imageUrl: '/images/icon-heart-new.png' + } + }, + + /** + * 静默记录分享奖励(分享人A获得+100爱心值) + */ + async recordShareReward() { + try { + const res = await api.lovePoints.share() + console.log('[index] 分享爱心值奖励:', res) + } catch (err) { + console.error('[index] 记录分享奖励失败:', err) + } + } +}) diff --git a/pages/index/index.json b/pages/index/index.json new file mode 100644 index 0000000..b16ef3e --- /dev/null +++ b/pages/index/index.json @@ -0,0 +1,4 @@ +{ + "navigationStyle": "custom", + "usingComponents": {} +} diff --git a/pages/index/index.wxml b/pages/index/index.wxml new file mode 100644 index 0000000..6c4dac4 --- /dev/null +++ b/pages/index/index.wxml @@ -0,0 +1,276 @@ + + + + + + 陪伴 + + + + + + + + + + + + + + + + + {{item.name}} + + + + + + + + 左右滑动 + + + + + + + + + + + + + + + + + + + + {{profiles[currentIndex].name}} + {{profiles[currentIndex].height}} + + + {{profiles[currentIndex].location}} + | + {{profiles[currentIndex].occupation}} + + {{profiles[currentIndex].bio}} + + {{item}} + + + + + + + + + + + + + {{profiles[currentIndex].name}} + + + + + + + + + 正在加载... + 请稍候 + + + + + + + + {{error || '暂时没有更多伙伴了'}} + 点击下方按钮重新加载 + + + + + + + + + 喜欢 + + + + 声音 + + + + {{unlockedProfiles[profiles[currentIndex].id] ? '已选' : '选择'}} + + + + + + + + + + + + + + + 解锁与 {{profiles[currentIndex].name}} 的专属聊天 + + + + + + + + + {{unlockHeartsCost}} 爱心 + {{heartCount >= unlockHeartsCost ? '爱心值充足 立即兑换' : '爱心值不足 去充值'}} + + 兑换 + + + + + ¥ + + + 9.9元 + 限时特惠 立即购买 + + 购买 + + + + 暂不需要 + + + + + + + + + × + + + + + + + + + + + + + + + + + + 解锁与 + {{currentCharacter.name}} + 的专属聊天 + + + + + + + + + + + + + + + + {{unlockHeartsCost}}爱心 + {{heartCount >= unlockHeartsCost ? '余额充足 立即兑换' : '爱心值不足 去充值'}} + + + + 兑换 + + + + + + 暂不需要 + + + + + + + + + + + × + + + + + + + + + 陪伴 + + + + 文娱 + + + + 服务 + + + + + + {{totalUnread}} + 99+ + + + 消息 + + + + 我的 + + + diff --git a/pages/index/index.wxss b/pages/index/index.wxss new file mode 100644 index 0000000..908d341 --- /dev/null +++ b/pages/index/index.wxss @@ -0,0 +1,1061 @@ +/* 首页样式 */ +.page-container { + min-height: 100vh; + background: linear-gradient(180deg, #E8C3D4 0%, #F5E6ED 100%); + position: relative; +} + +/* 顶部导航栏已移除,改用全局 unified-header */ +.nav-title-text { + font-size: 40rpx; + font-weight: bold; + color: #101828; + margin-left:40rpx +} + +/* 内容区域 */ +.content-area { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow-y: auto; /* 允许滚动 */ +} + +/* 顶部 Banner */ +.banner-section { + padding: 20rpx 32rpx 0; +} + +/* 顶部 Banner */ +.banner-section { + padding: 20rpx 32rpx 0; +} + +.home-banner { + width: 100%; + border-radius: 24rpx; + box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.1); + display: block; +} + +.story-scroll { + padding: 24rpx 0; + white-space: nowrap; +} + +.story-list { + display: inline-flex; + gap: 40rpx; + padding: 0 20rpx; +} + +.story-item { + display: flex; + flex-direction: column; + align-items: center; + width: 140rpx; +} + +.story-avatar-wrap { + position: relative; + width: 142rpx; + height: 142rpx; +} + +.story-ring { + position: absolute; + inset: 0; + border-radius: 50%; + background: linear-gradient(135deg, #DEE2E7 0%, #DBE0E7 100%); + box-shadow: 0 31rpx 31rpx rgba(142, 155, 174, 0.2); +} + +.story-avatar { + position: absolute; + top: 10rpx; + left: 10rpx; + width: 122rpx; + height: 122rpx; + border-radius: 50%; + border: 4rpx solid #fff; +} + +.story-name { + margin-top: 8rpx; + font-size: 26rpx; + font-weight: 600; + color: #1a1a1a; + text-align: center; +} + +/* 滑动提示 */ +.swipe-hint { + display: flex; + align-items: center; + justify-content: center; + gap: 16rpx; + padding: 16rpx 0; + opacity: 0.6; +} + +.hint-arrow { + width: 40rpx; + height: 40rpx; + opacity: 0.8; +} + +.hint-text { + font-size: 32rpx; + font-weight: 900; + color: #914584; + letter-spacing: 4rpx; +} + +/* 卡片堆叠 */ +.card-stack { + position: relative; + width: 100%; + height: 1040rpx; + display: flex; + justify-content: center; + margin-top: 16rpx; +} + +.card-next { + position: absolute; + top: 80rpx; + width: 706rpx; + height: 724rpx; + border-radius: 68rpx; + overflow: hidden; + transform: scale(0.95); + opacity: 0.8; + background: #f4f7fb; + box-shadow: 0 40rpx 80rpx rgba(59, 64, 86, 0.15); +} + +.card-bg-image { + width: 100%; + height: 100%; +} + +.card-current { + position: absolute; + top: 0; + width: 706rpx; + height: 800rpx; + transition: transform 0.1s ease-out; + z-index: 10; +} + +.card-current.swipe-left { + animation: swipeLeft 0.3s ease-out forwards; +} + +.card-current.swipe-right { + animation: swipeRight 0.3s ease-out forwards; +} + +@keyframes swipeLeft { + to { + transform: translateX(-1000rpx) rotate(-20deg); + opacity: 0; + } +} + +@keyframes swipeRight { + to { + transform: translateX(1000rpx) rotate(20deg); + opacity: 0; + } +} + +.card-inner { + position: relative; + width: 100%; + height: 724rpx; + margin-top: 52rpx; + border-radius: 68rpx; + overflow: hidden; + background: #f4f7fb; + box-shadow: 0 40rpx 80rpx rgba(59, 64, 86, 0.72); +} + +.card-image { + width: 100%; + height: 100%; +} + +/* 卡片信息覆盖层 */ +.card-overlay { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 96rpx 48rpx 48rpx; + background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.4) 50%, transparent 100%); +} + +.card-info { + color: #fff; +} + +.card-name-row { + display: flex; + align-items: baseline; + gap: 16rpx; + margin-bottom: 12rpx; +} + +.card-name { + font-size: 52rpx; + font-weight: 700; + text-shadow: 0 4rpx 8rpx rgba(0,0,0,0.3); +} + +.card-height { + font-size: 30rpx; + font-weight: 500; + opacity: 0.95; +} + +.card-location-row { + display: flex; + align-items: center; + gap: 16rpx; + font-size: 26rpx; + font-weight: 500; + opacity: 0.9; + margin-bottom: 20rpx; +} + +.card-divider { + opacity: 0.6; +} + +.card-bio { + font-size: 28rpx; + font-weight: 500; + opacity: 0.95; + line-height: 1.6; + margin-bottom: 20rpx; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.card-hobbies { + font-size: 26rpx; + font-weight: 500; + opacity: 0.8; + margin-bottom: 24rpx; +} + +.card-tags { + display: flex; + flex-wrap: wrap; + gap: 16rpx; +} + +.card-tag { + font-size: 22rpx; + font-weight: 700; + background: rgba(255,255,255,0.25); + backdrop-filter: blur(20rpx); + padding: 8rpx 20rpx; + border-radius: 14rpx; + border: 2rpx solid rgba(255,255,255,0.2); +} + +/* 悬浮操作按钮 - 放在卡片和底部导航栏之间 */ +.floating-actions { + position: fixed; + left: 0; + right: 0; + bottom: 230rpx; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 64rpx; + z-index: 50; + padding: 0 48rpx; +} + +.action-btn { + display: flex; + flex-direction: column; + align-items: center; + gap: 16rpx; + flex: 0 0 auto; +} + +.action-btn .action-icon { + width: 116rpx; + height: 116rpx; + background: rgba(0,0,0,0.3); + backdrop-filter: blur(20rpx); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + border: 3rpx solid rgba(255,255,255,0.3); + padding: 28rpx; + box-sizing: border-box; + box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.4); +} + +/* 爱心按钮 - 白色背景 */ +.heart-btn .action-icon { + background: #fff; + border: 3rpx solid rgba(255,255,255,0.3); + box-shadow: 0 8rpx 24rpx rgba(251, 44, 54, 0.3); +} + +/* 声音按钮 - 淡紫色半透明背景 */ +.voice-btn .action-icon { + background: rgba(145, 69, 132, 0.6); + border: 3rpx solid rgba(255,255,255,0.3); + box-shadow: 0 8rpx 24rpx rgba(145, 69, 132, 0.4); +} + +.action-btn.liked .action-icon { + background: #fff; +} + +.action-label { + font-size: 34rpx; + font-weight: 900; + color: #fff; + text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.8), 0 4rpx 12rpx rgba(0,0,0,0.6); + letter-spacing: 2rpx; +} + +.select-btn .action-icon { + background: linear-gradient(135deg, #4ade80 0%, #16a34a 100%); +} + +.select-btn.unlocked .action-icon { + background: linear-gradient(135deg, #22c55e 0%, #15803d 100%); + box-shadow: 0 0 0 4rpx #fff; +} + +/* 顶部头像 */ +.card-host { + position: absolute; + top: 14rpx; + left: 6rpx; + display: flex; + align-items: center; + gap: 20rpx; + z-index: 20; +} + +.host-avatar-wrap { + width: 142rpx; + height: 142rpx; + border-radius: 50%; + overflow: hidden; + border: 4rpx solid #fff; + box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.2); +} + +.host-avatar { + width: 100%; + height: 100%; +} + +.host-name { + font-size: 34rpx; + font-weight: 700; + color: #fff; + text-shadow: 0 4rpx 12rpx rgba(0,0,0,0.4); + padding: 20rpx; +} + +/* 空状态 */ +.card-empty { + position: absolute; + top: 80rpx; + width: 706rpx; + height: 724rpx; + border-radius: 68rpx; + background: #f4f7fb; + box-shadow: 0 40rpx 80rpx rgba(59, 64, 86, 0.15); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48rpx; +} + +.empty-icon-wrap { + width: 192rpx; + height: 192rpx; + background: #fff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 16rpx 48rpx rgba(0,0,0,0.1); + margin-bottom: 32rpx; +} + +.empty-icon-wrap.loading { + background: linear-gradient(135deg, #E9D5FF 0%, #FDF2F8 100%); +} + +.empty-icon { + width: 80rpx; + height: 80rpx; +} + +.empty-icon.rotating { + animation: rotate 1.5s linear infinite; +} + +@keyframes rotate { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.empty-title { + font-size: 40rpx; + font-weight: 700; + color: #1f2937; + margin-bottom: 16rpx; +} + +.empty-desc { + font-size: 28rpx; + color: #6b7280; + margin-bottom: 48rpx; +} + +.refresh-btn { + background: #914584; + color: #fff; + font-size: 36rpx; + font-weight: 700; + padding: 24rpx 64rpx; + border-radius: 100rpx; + border: none; + box-shadow: 0 16rpx 32rpx rgba(145, 69, 132, 0.3); +} + +/* 弹窗 */ +.modal-mask { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.5); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; +} + +.modal-content { + width: 640rpx; + background: #f8f9fc; + border-radius: 48rpx; + overflow: hidden; +} + +.modal-header { + background: #fff; + padding: 64rpx 48rpx 40rpx; + text-align: center; + border-radius: 0 0 60rpx 60rpx; + box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.05); + margin-bottom: 32rpx; +} + +.modal-avatar-wrap { + position: relative; + width: 192rpx; + height: 192rpx; + margin: 0 auto 40rpx; +} + +.modal-avatar { + width: 100%; + height: 100%; + border-radius: 50%; + border: 6rpx solid #fff; + box-shadow: 0 16rpx 32rpx rgba(0,0,0,0.15); +} + +.modal-lock { + position: absolute; + bottom: -8rpx; + right: -8rpx; + width: 48rpx; + height: 48rpx; + background: #fff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.15); +} + +.lock-icon { + width: 32rpx; + height: 32rpx; +} + +.modal-title { + font-size: 42rpx; + font-weight: 800; + color: #111827; + line-height: 1.4; +} + +.modal-title .highlight { + color: #914584; +} + +.modal-options { + padding: 0 40rpx 24rpx; +} + +.option-item { + background: #fff; + border-radius: 32rpx; + padding: 32rpx; + display: flex; + align-items: center; + gap: 28rpx; + margin-bottom: 24rpx; + box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.05); + border: 2rpx solid #f3f4f6; +} + +.option-item.highlight { + background: linear-gradient(135deg, #914584 0%, #7a3a6f 100%); + border: none; + box-shadow: 0 16rpx 32rpx rgba(145, 69, 132, 0.3); +} + +.option-icon { + width: 88rpx; + height: 88rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.option-icon.grass { + background: #dcfce7; +} + +.option-icon.grass image { + width: 44rpx; + height: 44rpx; +} + +.option-icon.money { + background: rgba(255,255,255,0.2); +} + +.money-symbol { + font-size: 48rpx; + font-weight: 800; + color: #fff; +} + +.option-info { + flex: 1; +} + +.option-price { + font-size: 40rpx; + font-weight: 800; + color: #111827; + display: block; +} + +.option-desc { + font-size: 30rpx; + font-weight: 600; + color: #6b7280; + display: block; + margin-top: 4rpx; +} + +.option-info.light .option-price, +.option-info.light .option-desc { + color: #fff; +} + +.option-info.light .option-desc { + opacity: 0.95; +} + +.option-btn { + background: #f3f4f6; + color: #4b5563; + font-size: 36rpx; + font-weight: 800; + padding: 16rpx 40rpx; + border-radius: 100rpx; +} + +.option-btn.light { + background: #fff; + color: #914584; + box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1); +} + +.modal-cancel { + display: block; + text-align: center; + font-size: 32rpx; + font-weight: 600; + color: #9ca3af; + padding: 20rpx 0 48rpx; +} + + +/* 自定义底部导航栏 - 完全匹配Figma设计 */ +.custom-tabbar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 194rpx; + background: #fff; + display: flex; + align-items: flex-start; + justify-content: space-around; + padding-top: 24rpx; + z-index: 999; + border-top: 2rpx solid #F3F4F6; +} + +.tabbar-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12rpx; + width: 150rpx; + height: 120rpx; +} + +.tabbar-icon { + width: 68rpx; + height: 68rpx; +} + +.tabbar-text { + font-family: Arial, sans-serif; + font-size: 40rpx; + font-weight: 700; + color: #A58AA5; + line-height: 1; +} + +.tabbar-text.active { + color: #B06AB3; +} + +.message-icon-wrapper { + position: relative; + width: 68rpx; + height: 68rpx; +} + +.message-icon-wrapper .tabbar-icon { + width: 68rpx; + height: 68rpx; +} + +.message-dot { + position: absolute; + top: -8rpx; + right: -8rpx; + width: 24rpx; + height: 24rpx; + background: #FB2C36; + border: 2rpx solid #fff; + border-radius: 50%; +} + +/* 消息数字角标 */ +.message-badge { + position: absolute; + top: -10rpx; + right: -16rpx; + min-width: 36rpx; + height: 36rpx; + padding: 0 8rpx; + background: #FB2C36; + border: 3rpx solid #fff; + border-radius: 18rpx; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; +} + +.message-badge text { + font-size: 22rpx; + font-weight: 600; + color: #fff; + line-height: 1; +} + + + +/* ==================== 解锁弹窗样式 - 完全匹配Figma设计 ==================== */ + +/* 弹窗遮罩 */ +.unlock-popup-mask { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1001; + display: flex; + align-items: center; + justify-content: center; +} + +/* 弹窗主体 */ +.unlock-popup { + width: 680rpx; + background: #F8F9FC; + border-radius: 48rpx; + overflow: hidden; + position: relative; + box-shadow: 0 50rpx 100rpx -24rpx rgba(0, 0, 0, 0.25); + animation: unlockPopupIn 0.3s ease-out; + min-height: 720rpx; +} + +@keyframes unlockPopupIn { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +/* 关闭按钮 */ +.unlock-popup-close { + position: absolute; + top: 32rpx; + right: 32rpx; + width: 32rpx; + height: 32rpx; + display: flex; + justify-content: center; + align-items: center; + opacity: 0.7; + z-index: 10; +} + +.unlock-popup-close text { + font-size: 40rpx; + color: #0A0A0A; + line-height: 1; + font-weight: 300; +} + +/* 顶部白色区域 */ +.unlock-popup-header { + background: #fff; + padding: 56rpx 48rpx 48rpx; + border-radius: 0 0 60rpx 60rpx; + box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + align-items: center; +} + +/* 头像容器 */ +.unlock-avatar-container { + position: relative; + width: 192rpx; + height: 192rpx; + margin-bottom: 40rpx; +} + +.unlock-avatar-wrap { + width: 192rpx; + height: 192rpx; + border-radius: 50%; + overflow: hidden; + border: 4rpx solid #fff; + box-shadow: 0 20rpx 30rpx -6rpx rgba(0, 0, 0, 0.1), 0 8rpx 12rpx -4rpx rgba(0, 0, 0, 0.1); +} + +.unlock-avatar { + width: 100%; + height: 100%; +} + +/* 锁图标 */ +.unlock-lock-icon { + position: absolute; + bottom: -8rpx; + right: -8rpx; + width: 56rpx; + height: 56rpx; + background: #fff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4rpx 12rpx -2rpx rgba(0, 0, 0, 0.1), 0 8rpx 12rpx -2rpx rgba(0, 0, 0, 0.1); +} + +.unlock-lock-icon image { + width: 32rpx; + height: 32rpx; +} + +/* 标题 */ +.unlock-title { + text-align: center; + font-size: 42rpx; + font-weight: 900; + color: #101828; + line-height: 1.25; + letter-spacing: -0.5rpx; +} + +.unlock-title .highlight { + color: #914584; +} + +/* 选项区域 */ +.unlock-options { + padding: 56rpx 40rpx 64rpx; + display: flex; + flex-direction: column; + gap: 32rpx; +} + +/* 选项卡通用样式 */ +.unlock-option-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 32rpx; + height: 150rpx; + border-radius: 32rpx; + box-shadow: 0 2rpx 4rpx -2rpx rgba(0, 0, 0, 0.1), 0 2rpx 6rpx rgba(0, 0, 0, 0.1); +} + +.option-left { + display: flex; + align-items: center; + gap: 28rpx; + flex: 1; +} + +/* 图标容器 */ +.option-icon { + width: 96rpx; + height: 96rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + box-shadow: 0 2rpx 4rpx -2rpx rgba(0, 0, 0, 0.1), 0 2rpx 6rpx 0 rgba(0, 0, 0, 0.1); +} + +/* 分享图标 */ +.share-icon { + background: #FFF0F5; +} + +.share-icon image { + width: 52rpx; + height: 52rpx; +} + +/* 爱心图标 */ +.hearts-icon { + background: #FFF0F5; +} + +.hearts-icon image { + width: 52rpx; + height: 52rpx; +} + +/* 选项信息 */ +.option-info { + display: flex; + flex-direction: column; + gap: 8rpx; + flex: 1; + min-width: 0; +} + +.option-title { + font-size: 36rpx; + font-weight: 900; + line-height: 1.4; + letter-spacing: -0.025em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.option-desc { + font-size: 28rpx; + font-weight: 700; + line-height: 1.5; + opacity: 0.8; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 按钮 */ +.option-btn { + width: 174rpx; + height: 100rpx; + border-radius: 100rpx; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + box-shadow: 0 4rpx 8rpx -4rpx rgba(0, 0, 0, 0.1), 0 8rpx 12rpx -2rpx rgba(0, 0, 0, 0.1); +} + +.option-btn text { + font-size: 40rpx; + font-weight: 900; + letter-spacing: -0.025em; + line-height: 1.5; +} + +/* ==================== GF100 弹窗样式 ==================== */ +.gf100-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.75); + display: flex; + align-items: center; + justify-content: center; + z-index: 2000; + backdrop-filter: blur(5px); +} + +.gf100-content { + position: relative; + width: 62.5%; + max-width: 480rpx; + animation: gf100In 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes gf100In { + from { + transform: scale(0.5); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +.gf100-image { + width: 100%; + display: block; + border-radius: 20rpx; + box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.5); +} + +.gf100-close { + position: absolute; + top: -80rpx; + right: 0; + width: 60rpx; + height: 60rpx; + border: 2rpx solid #fff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.close-icon { + color: #fff; + font-size: 40rpx; + line-height: 1; +} + + +/* 分享选项 - 粉色背景 */ +.share-option { + background: #FFF0F5; + border: 2rpx solid #FCE7F3; +} + +.share-option .option-title { + color: #914584; +} + +.share-option .option-desc { + color: #914584; +} + +.share-btn { + background: #914584; +} + +.share-btn text { + color: #FFFFFF; +} + +/* 爱心选项 - 白色背景 */ +.hearts-option { + background: #FFFFFF; + border: 2rpx solid #F3F4F6; +} + +.hearts-option .option-title { + color: #101828; +} + +.hearts-option .option-desc { + color: #6A7282; +} + +.hearts-btn { + background: #F3F4F6; +} + +.hearts-btn text { + color: #4A5565; +} + +/* 暂不需要 */ +.unlock-cancel { + text-align: center; + padding: 24rpx 0 0; +} + +.unlock-cancel text { + font-size: 32rpx; + font-weight: 700; + color: #99A1AF; + letter-spacing: 0.5rpx; + line-height: 1.5; +} diff --git a/pages/interest-partner/interest-partner.js b/pages/interest-partner/interest-partner.js new file mode 100644 index 0000000..477b5b0 --- /dev/null +++ b/pages/interest-partner/interest-partner.js @@ -0,0 +1,281 @@ +// pages/interest-partner/interest-partner.js - 兴趣搭子页面(Figma设计) +const api = require('../../utils/api') +const config = require('../../config/index') +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + + // 二维码弹窗 + showQrcodeModal: false, + selectedPartner: null, // 当前选中的搭子对象 + + // 兴趣搭子列表 + partnerList: [], + loading: false, + + // 固定的6个分类(与Figma设计一致) + fixedCategories: [ + { name: '美食聚餐', icon: 'food-icon', desc: '同城美食 共享美味' }, + { name: '旅游出行', icon: 'travel-icon', desc: '结伴出游 共度美好' }, + { name: '唱歌观影', icon: 'entertainment-icon', desc: '老歌金曲 经典影视' }, + { name: '舞蹈走秀', icon: 'dance-icon', desc: '展现风采 舞动人生' }, + { name: '书画摄影', icon: 'art-icon', desc: '陶冶情操 记录生活之美' }, + { name: '运动康养', icon: 'sports-icon', desc: '多种运动 身心健康' } + ] + }, + + onLoad() { + // 计算导航栏高度 + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight + }) + + // 加载兴趣搭子列表 + this.loadPartnerList() + }, + + /** + * 页面显示时刷新数据 + */ + onShow() { + // 每次显示页面时重新加载数据,确保数据是最新的 + this.loadPartnerList() + }, + + /** + * 下拉刷新 + */ + onPullDownRefresh() { + console.log('下拉刷新兴趣搭子列表') + this.loadPartnerList().then(() => { + wx.stopPullDownRefresh() + wx.showToast({ + title: '刷新成功', + icon: 'success' + }) + }).catch(() => { + wx.stopPullDownRefresh() + }) + }, + + /** + * 加载兴趣搭子列表 + */ + async loadPartnerList() { + if (this.data.loading) return + + this.setData({ loading: true }) + + try { + // 添加时间戳参数,防止缓存 + const timestamp = Date.now() + const res = await api.interest.getList({ _t: timestamp }) + + console.log('[兴趣搭子] API原始响应:', JSON.stringify(res).substring(0, 500)) + + // 线上API返回格式:{ success: true, data: [...] } 或 { success: true, data: { list: [...] } } + if (res.success && res.data) { + // 兼容两种返回格式 + let partnerList = Array.isArray(res.data) ? res.data : (res.data.list || []) + + console.log('[兴趣搭子] 解析后的列表数量:', partnerList.length) + if (partnerList.length > 0) { + console.log('[兴趣搭子] 第一条数据示例:', JSON.stringify(partnerList[0])) + } + + // 处理图片URL - 根据API文档,icon和qr_code已经是完整的API路径 + partnerList = partnerList.map(item => { + // 处理icon字段 + let iconUrl = '/images/icon-interest-default.png' // 默认图标 + if (item.icon && item.icon.trim()) { + if (item.icon.startsWith('http://') || item.icon.startsWith('https://')) { + // 已经是完整URL + iconUrl = item.icon + } else if (item.icon.startsWith('/')) { + // 相对路径,需要拼接域名 + iconUrl = `https://ai-c.maimanji.com${item.icon}` + } else { + // 不以/开头,添加/再拼接 + iconUrl = `https://ai-c.maimanji.com/${item.icon}` + } + } + + // 处理qr_code字段 + let qrCodeUrl = '' + if (item.qr_code && item.qr_code.trim()) { + if (item.qr_code.startsWith('http://') || item.qr_code.startsWith('https://')) { + qrCodeUrl = item.qr_code + } else if (item.qr_code.startsWith('/')) { + qrCodeUrl = `https://ai-c.maimanji.com${item.qr_code}` + } else { + qrCodeUrl = `https://ai-c.maimanji.com/${item.qr_code}` + } + } + + console.log(`[兴趣搭子] ${item.name} - 原始icon: ${item.icon}, 处理后: ${iconUrl}`) + + return { + ...item, + icon: iconUrl, + qr_code: qrCodeUrl + } + }) + + console.log(`[${new Date().toLocaleTimeString()}] 兴趣搭子列表加载成功,共 ${partnerList.length} 条数据`) + this.setData({ + partnerList, + loading: false + }) + + // 返回Promise以支持下拉刷新 + return Promise.resolve() + } else { + console.warn('兴趣搭子列表返回格式异常:', res) + this.setData({ loading: false }) + wx.showToast({ + title: '加载失败', + icon: 'none' + }) + return Promise.reject() + } + } catch (err) { + console.error('加载兴趣搭子列表失败:', err) + this.setData({ loading: false }) + wx.showToast({ + title: '网络错误', + icon: 'none' + }) + return Promise.reject(err) + } + }, + + /** + * 返回上一页 + */ + onBack() { + wx.navigateBack() + }, + + /** + * 点击兴趣卡片 + */ + onInterestTap(e) { + const { partner } = e.currentTarget.dataset + + if (!partner) { + wx.showToast({ + title: '数据加载中', + icon: 'none' + }) + return + } + + // 检查是否有二维码 + if (!partner.qr_code || !partner.qr_code.trim()) { + wx.showToast({ + title: '二维码暂未配置', + icon: 'none' + }) + return + } + + // 二维码URL已在loadPartnerList中处理过,直接使用 + this.setData({ + showQrcodeModal: true, + selectedPartner: partner + }) + }, + + /** + * 关闭二维码弹窗 + */ + onCloseQrcodeModal() { + this.setData({ + showQrcodeModal: false, + selectedPartner: null + }) + }, + + /** + * 保存二维码 + */ + onSaveQrcode() { + const { selectedPartner } = this.data + + if (!selectedPartner || !selectedPartner.qr_code) { + wx.showToast({ + title: '二维码加载中', + icon: 'none' + }) + return + } + + wx.showLoading({ title: '保存中...' }) + + // 下载图片 + wx.downloadFile({ + url: selectedPartner.qr_code, + success: (res) => { + if (res.statusCode === 200) { + // 保存到相册 + wx.saveImageToPhotosAlbum({ + filePath: res.tempFilePath, + success: () => { + wx.hideLoading() + wx.showToast({ + title: '已保存到相册', + icon: 'success' + }) + this.onCloseQrcodeModal() + }, + fail: (err) => { + wx.hideLoading() + if (err.errMsg.includes('auth deny')) { + wx.showModal({ + title: '需要授权', + content: '请允许访问相册以保存二维码', + confirmText: '去设置', + success: (modalRes) => { + if (modalRes.confirm) { + wx.openSetting() + } + } + }) + } else { + wx.showToast({ + title: '保存失败', + icon: 'none' + }) + } + } + }) + } else { + wx.hideLoading() + wx.showToast({ + title: '下载失败', + icon: 'none' + }) + } + }, + fail: () => { + wx.hideLoading() + wx.showToast({ + title: '下载失败', + icon: 'none' + }) + } + }) + } +}) diff --git a/pages/interest-partner/interest-partner.json b/pages/interest-partner/interest-partner.json new file mode 100644 index 0000000..28e169a --- /dev/null +++ b/pages/interest-partner/interest-partner.json @@ -0,0 +1,6 @@ +{ + "navigationStyle": "custom", + "navigationBarTextStyle": "black", + "enablePullDownRefresh": true, + "backgroundColor": "#F2EDFF" +} diff --git a/pages/interest-partner/interest-partner.wxml b/pages/interest-partner/interest-partner.wxml new file mode 100644 index 0000000..17db869 --- /dev/null +++ b/pages/interest-partner/interest-partner.wxml @@ -0,0 +1,106 @@ + + + + + + + 返回 + + 兴趣搭子 + + + + + + + + + + 寻找志同道合的伙伴 + 加入感兴趣的社群,开启精彩退休生活 + + + + + + + + + + + + + {{item.name}} + + + {{item.member_count}} + + + {{item.description}} + + + + + 加载中... + + + + 暂无兴趣搭子数据 + + + + + + + + + + + + + + + + 如何加入? + 点击上方感兴趣的分类,保存二维码图片或直接扫码,即可加入我们的官方企业微信社群。 + + + + + + + + + + + + + + + + + + + + + {{selectedPartner.group_name || '加入兴趣群'}} + + + {{selectedPartner.group_description || '找到志同道合的伙伴'}} + + + + + + + + + 保存二维码 + + + + diff --git a/pages/interest-partner/interest-partner.wxss b/pages/interest-partner/interest-partner.wxss new file mode 100644 index 0000000..59f734a --- /dev/null +++ b/pages/interest-partner/interest-partner.wxss @@ -0,0 +1,489 @@ +/* 兴趣搭子页面样式 - Figma设计 */ +page { + background: #F8F8F8; +} + +.page-container { + min-height: 100vh; + background: #F8F8F8; +} + +/* 顶部导航栏已移除,改用全局 unified-header */ + +/* 内容滚动区域 */ +.content-scroll { + height: 100vh; + box-sizing: border-box; +} + +/* 顶部Banner */ +.hero-banner { + margin: 32rpx 32rpx 48rpx; + height: 240rpx; + border-radius: 32rpx; + position: relative; + overflow: hidden; + background: linear-gradient(90deg, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.2) 50%, rgba(0, 0, 0, 0) 100%), + url('https://images.unsplash.com/photo-1529156069898-49953e39b3ac?w=800') center/cover; + box-shadow: 0px 8rpx 12rpx -8rpx rgba(0, 0, 0, 0.1), + 0px 20rpx 30rpx -6rpx rgba(0, 0, 0, 0.1), + 0px 0px 0px 2rpx rgba(0, 0, 0, 0.05); +} + +.hero-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.2) 50%, rgba(0, 0, 0, 0) 100%); +} + +.hero-content { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + justify-content: center; + gap: 16rpx; + padding: 0 48rpx; + height: 100%; +} + +.hero-title { + font-size: 52rpx; + font-weight: 900; + color: #FFFFFF; + line-height: 1.5; + letter-spacing: 0.025em; + text-shadow: 0px 6rpx 12rpx rgba(0, 0, 0, 0.12); +} + +.hero-subtitle { + font-size: 34rpx; + font-weight: 700; + color: rgba(255, 255, 255, 0.95); + line-height: 1.5; + letter-spacing: 0.025em; + text-shadow: 0px 2rpx 8rpx rgba(0, 0, 0, 0.15); +} + +/* 兴趣分类列表 */ +.interest-list { + padding: 0 32rpx; + display: flex; + flex-direction: column; + gap: 32rpx; +} + +.interest-card { + display: flex; + align-items: center; + gap: 40rpx; + padding: 2rpx 42rpx; + height: 238rpx; + background: #FFFFFF; + border: 2.324rpx solid #F1F5F9; + border-radius: 40rpx; + box-shadow: 0px 2rpx 6rpx 0px rgba(0, 0, 0, 0.1), + 0px 2rpx 4rpx -2rpx rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; +} + +.interest-card:active { + transform: scale(0.98); + box-shadow: 0px 1rpx 2rpx -1rpx rgba(0, 0, 0, 0.1); +} + +/* 图标样式 */ +.interest-icon { + width: 152rpx; + height: 152rpx; + border-radius: 52rpx; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + position: relative; + box-sizing: border-box; +} + +/* 图标图片 */ +.icon-image { + width: 152rpx; + height: 152rpx; +} + +/* 加载提示 */ +.loading-tip { + text-align: center; + padding: 80rpx 0; + font-size: 28rpx; + color: #999; +} + +/* 空状态提示 */ +.empty-tip { + text-align: center; + padding: 120rpx 0; + font-size: 28rpx; + color: #999; +} + +/* 保留原有的固定分类图标样式作为备用 */ + +/* 美食聚餐 - 橙色 */ +.food-icon { + background: #FFF7ED; +} + +.food-icon::before { + content: ''; + position: absolute; + width: 80rpx; + height: 80rpx; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23FF7A45' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 8h1a4 4 0 0 1 0 8h-1'/%3E%3Cpath d='M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z'/%3E%3Cline x1='6' y1='1' x2='6' y2='4'/%3E%3Cline x1='10' y1='1' x2='10' y2='4'/%3E%3Cline x1='14' y1='1' x2='14' y2='4'/%3E%3C/svg%3E"); + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} + +/* 旅游出行 - 青色 */ +.travel-icon { + background: #ECFEFF; +} + +.travel-icon::before { + content: ''; + position: absolute; + width: 80rpx; + height: 80rpx; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2336CFC9' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z'/%3E%3Cpolyline points='7.5 4.21 12 6.81 16.5 4.21'/%3E%3Cpolyline points='7.5 19.79 7.5 14.6 3 12'/%3E%3Cpolyline points='21 12 16.5 14.6 16.5 19.79'/%3E%3Cpolyline points='3.27 6.96 12 12.01 20.73 6.96'/%3E%3Cline x1='12' y1='22.08' x2='12' y2='12'/%3E%3C/svg%3E"); + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} + +/* 唱歌观影 - 紫色 */ +.entertainment-icon { + background: #FAF5FF; +} + +.entertainment-icon::before { + content: ''; + position: absolute; + width: 80rpx; + height: 80rpx; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239254DE' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 18V5l12-2v13'/%3E%3Ccircle cx='6' cy='18' r='3'/%3E%3Ccircle cx='18' cy='16' r='3'/%3E%3C/svg%3E"); + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} + +/* 舞蹈走秀 - 粉色 */ +.dance-icon { + background: #FDF2F8; +} + +.dance-icon::before { + content: ''; + position: absolute; + width: 80rpx; + height: 80rpx; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23F759AB' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='5' r='2'/%3E%3Cpath d='M10 22v-5l-1-1v-4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v4l-1 1v5'/%3E%3C/svg%3E"); + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} + +/* 书画摄影 - 蓝色 */ +.art-icon { + background: #EEF2FF; +} + +.art-icon::before { + content: ''; + position: absolute; + width: 80rpx; + height: 80rpx; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23597EF7' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z'/%3E%3Ccircle cx='12' cy='13' r='4'/%3E%3C/svg%3E"); + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} + +/* 运动康养 - 绿色 */ +.sports-icon { + background: #F0FDF4; +} + +.sports-icon::before { + content: ''; + position: absolute; + width: 80rpx; + height: 80rpx; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2373D13D' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 12h-4l-3 9L9 3l-3 9H2'/%3E%3C/svg%3E"); + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} + +/* 信息区域 */ +.interest-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.interest-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 24rpx; +} + +.interest-name { + font-size: 48rpx; + font-weight: 700; + color: #1D293D; + line-height: 1.5; + letter-spacing: -0.025em; +} + +.interest-members { + display: flex; + align-items: center; + gap: 12rpx; + flex-shrink: 0; +} + +.members-icon { + width: 40rpx; + height: 40rpx; +} + +.members-count { + font-size: 32rpx; + font-weight: 700; + color: #62748E; + line-height: 1.5; +} + +.interest-desc { + font-size: 44rpx; + font-weight: 700; + color: #45556C; + line-height: 1.5; +} + +/* 如何加入说明 */ +.how-to-join { + display: flex; + align-items: flex-start; + gap: 24rpx; + padding: 32rpx 32rpx 32rpx 32rpx; + margin: 0 32rpx 0; + background: #FFF7ED; + border-radius: 32rpx; +} + +.join-icon-wrapper { + flex-shrink: 0; + width: 72rpx; + height: 72rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.join-icon { + position: relative; + width: 40rpx; + height: 40rpx; +} + +.dot { + position: absolute; + width: 6.66rpx; + height: 6.66rpx; + background: transparent; + border: 3.33rpx solid #F54900; + border-radius: 50%; +} + +.dot-1 { + top: 5rpx; + left: 5rpx; +} + +.dot-2 { + top: 5rpx; + right: 5rpx; +} + +.dot-3 { + bottom: 5rpx; + right: 5rpx; +} + +.dot-4 { + bottom: 5rpx; + left: 5rpx; +} + +.line { + position: absolute; + top: 50%; + left: 11.66rpx; + width: 16.66rpx; + height: 0; + border-top: 3.33rpx solid #F54900; + transform: translateY(-50%); +} + +.join-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 8rpx; + padding-right: 32rpx; +} + +.join-title { + font-size: 32rpx; + font-weight: 700; + color: #1D293D; + line-height: 1.5; +} + +.join-desc { + font-size: 28rpx; + font-weight: 400; + color: #45556C; + line-height: 1.625; +} + +/* 二维码弹窗 */ +.qrcode-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; +} + +/* 遮罩层 */ +.modal-mask { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4rpx); +} + +/* 弹窗内容 */ +.modal-content { + position: relative; + width: 680rpx; + background: #FFFFFF; + border-radius: 64rpx; + padding: 64rpx; + box-shadow: 0 50rpx 100rpx -24rpx rgba(0, 0, 0, 0.25); + display: flex; + flex-direction: column; + align-items: center; + z-index: 1; +} + +/* 关闭按钮 */ +.close-btn { + position: absolute; + top: 32rpx; + right: 32rpx; + width: 72rpx; + height: 72rpx; + background: #F1F5F9; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.close-btn:active { + transform: scale(0.9); + background: #E2E8F0; +} + +.close-icon { + width: 40rpx; + height: 40rpx; +} + +/* 标题 */ +.modal-title { + font-size: 48rpx; + font-weight: 700; + color: #1D293D; + text-align: center; + margin-bottom: 16rpx; + line-height: 1.5; +} + +/* 副标题 */ +.modal-subtitle { + font-size: 32rpx; + color: #62748E; + text-align: center; + margin-bottom: 48rpx; + line-height: 1.5; +} + +/* 二维码容器 */ +.qrcode-container { + width: 440rpx; + height: 440rpx; + background: #F8FAFC; + border: 2rpx solid #F1F5F9; + border-radius: 40rpx; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 48rpx; + overflow: hidden; +} + +.qrcode-image { + width: 404rpx; + height: 404rpx; + border-radius: 24rpx; +} + +/* 保存按钮 */ +.save-btn { + width: 552rpx; + height: 116rpx; + background: #07C160; + border-radius: 100rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 40rpx; + font-weight: 700; + color: #FFFFFF; + box-shadow: 0 20rpx 30rpx -6rpx rgba(220, 252, 231, 1), + 0 8rpx 12rpx -8rpx rgba(220, 252, 231, 1); + transition: all 0.3s ease; +} + +.save-btn:active { + transform: scale(0.96); + box-shadow: 0 10rpx 20rpx -6rpx rgba(220, 252, 231, 1); +} diff --git a/pages/invite/invite.js b/pages/invite/invite.js new file mode 100644 index 0000000..633c5d2 --- /dev/null +++ b/pages/invite/invite.js @@ -0,0 +1,662 @@ +const api = require('../../utils/api') +const auth = require('../../utils/auth') +const config = require('../../config/index') + +Page({ + data: { + invitationCode: '', + referralCode: '', // 推荐码(佣金系统) + shareUrl: '', + stats: { + total_invites: 0, + completed_invites: 0 + }, + // 佣金统计 + commissionStats: { + totalReferrals: 0, + commissionBalance: 0, + totalEarned: 0 + }, + lovePoints: 0, + shareCount: 0, + maxShareCount: 3, + loading: false, + isDistributor: false, // 是否是分销商 + // 海报相关 + posterTemplates: [], + currentPosterIndex: 0, + generatingPoster: false, + shareConfig: null + }, + + async onLoad() { + // 统一登录验证 + const isValid = await auth.ensureLogin({ + pageName: 'invite', + redirectUrl: '/pages/invite/invite' + }) + + if (!isValid) return + + // 验证通过后,稍作延迟确保token稳定 + await new Promise(resolve => setTimeout(resolve, 50)) + + // 加载数据 + this.loadData() + }, + + onShow() { + // 每次显示页面时刷新数据(已登录的情况下) + const app = getApp() + if (app.globalData.isLoggedIn) { + this.loadData() + } + }, + + /** + * 加载所有数据 + */ + async loadData() { + if (this.data.loading) return + + this.setData({ loading: true }) + + try { + await Promise.all([ + this.loadInvitationCode(), + this.loadCommissionStats(), // 加载佣金统计 + this.getLovePoints(), + this.getTodayShareCount(), + this.loadPosterTemplates(), + this.loadShareConfig() + ]) + } catch (error) { + console.error('加载数据失败:', error) + } finally { + this.setData({ loading: false }) + } + }, + + /** + * 获取邀请码 + */ + async loadInvitationCode() { + try { + const res = await api.lovePoints.getInvitationCode() + if (res.success) { + this.setData({ + invitationCode: res.data.invitation_code, + shareUrl: res.data.share_url, + stats: res.data.stats + }) + } + } catch (error) { + console.error('获取邀请码失败:', error) + // 401错误由API层统一处理,这里只处理其他错误 + if (error.code !== 401) { + wx.showToast({ + title: error.message || '获取邀请码失败', + icon: 'none' + }) + } + } + }, + + /** + * 加载佣金统计数据 + */ + async loadCommissionStats() { + try { + const res = await api.commission.getStats() + if (res.success && res.data) { + this.setData({ + referralCode: res.data.referralCode || '', + isDistributor: res.data.isDistributor || false, + commissionStats: { + totalReferrals: res.data.totalReferrals || 0, + commissionBalance: res.data.commissionBalance || 0, + totalEarned: res.data.totalEarned || 0 + } + }) + } + } catch (error) { + console.error('获取佣金统计失败:', error) + // 不显示错误提示,静默失败 + } + }, + + /** + * 获取爱心值余额(使用后端新接口) + */ + getLovePoints() { + return new Promise((resolve, reject) => { + const token = wx.getStorageSync('auth_token') + if (!token) { + resolve() + return + } + + wx.request({ + url: `${config.API_BASE_URL}/love-points/balance`, + method: 'GET', + header: { + 'Authorization': `Bearer ${token}` + }, + success: (res) => { + console.log('爱心值余额API响应:', res.data) + if (res.data.success) { + this.setData({ + lovePoints: res.data.data.love_points + }) + resolve(res.data.data) + } else { + reject(new Error(res.data.error)) + } + }, + fail: reject + }) + }) + }, + + /** + * 获取今日分享次数(使用后端新接口) + */ + getTodayShareCount() { + return new Promise((resolve, reject) => { + const token = wx.getStorageSync('auth_token') + if (!token) { + resolve() + return + } + + wx.request({ + url: `${config.API_BASE_URL}/love-points/share-count`, + method: 'GET', + header: { + 'Authorization': `Bearer ${token}` + }, + success: (res) => { + console.log('分享次数API响应:', res.data) + if (res.data.success) { + this.setData({ + shareCount: res.data.data.count, + maxShareCount: res.data.data.max + }) + resolve(res.data.data) + } else { + reject(new Error(res.data.error)) + } + }, + fail: reject + }) + }) + }, + + /** + * 复制邀请码 + */ + copyInviteCode() { + const { isDistributor, referralCode, invitationCode } = this.data; + // 如果是分销商且有推荐码,优先复制推荐码 + const codeToCopy = (isDistributor && referralCode) ? referralCode : invitationCode; + + if (!codeToCopy) { + wx.showToast({ + title: '邀请码加载中...', + icon: 'none' + }) + return + } + + wx.setClipboardData({ + data: codeToCopy, + success: () => { + wx.showToast({ + title: '邀请码已复制', + icon: 'success' + }) + } + }) + }, + + /** + * 查看佣金中心 + */ + goToCommission() { + wx.navigateTo({ + url: '/pages/commission/commission' + }) + }, + + /** + * 点击分享按钮 + */ + onShareTap() { + console.log('用户点击了分享按钮') + + // 检查登录状态 + const token = wx.getStorageSync('auth_token') + if (!token) { + wx.showModal({ + title: '提示', + content: '请先登录', + confirmText: '去登录', + success: (res) => { + if (res.confirm) { + wx.navigateTo({ + url: '/pages/login/login' + }) + } + } + }) + return + } + + // 检查是否达到上限 + if (this.data.shareCount >= this.data.maxShareCount) { + wx.showToast({ + title: '今日分享次数已达上限', + icon: 'none', + duration: 2000 + }) + return + } + + // 立即记录分享 + this.recordShare() + }, + + /** + * 记录分享并获得爱心值(使用后端新接口) + */ + recordShare() { + const token = wx.getStorageSync('auth_token') + if (!token) return + + wx.showLoading({ + title: '处理中...', + mask: true + }) + + wx.request({ + url: `${config.API_BASE_URL}/love-points/share`, + method: 'POST', + header: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + success: (res) => { + wx.hideLoading() + console.log('分享奖励API响应:', res.data) + + if (res.data.success) { + const newShareCount = this.data.shareCount + 1 + const newLovePoints = this.data.lovePoints + res.data.data.earned + + this.setData({ + shareCount: newShareCount, + lovePoints: newLovePoints + }) + + wx.showToast({ + title: `+${res.data.data.earned} 爱心值`, + icon: 'success', + duration: 2000 + }) + + const app = getApp() + if (app.globalData) { + app.globalData.lovePoints = newLovePoints + } + + this.triggerPageRefresh() + } else { + wx.showToast({ + title: res.data.error || '分享失败', + icon: 'none', + duration: 2000 + }) + } + }, + fail: (error) => { + wx.hideLoading() + console.error('分享奖励API调用失败:', error) + wx.showToast({ + title: '网络错误,请稍后重试', + icon: 'none', + duration: 2000 + }) + } + }) + }, + + /** + * 静默记录分享奖励(用于 onShareAppMessage/onShareTimeline) + * 不显示 loading 和 Toast,后台静默调用 + * 分享人A获得+100爱心值 + */ + async recordShareReward() { + try { + const token = wx.getStorageSync('auth_token') + if (!token) return + + const res = await new Promise((resolve, reject) => { + wx.request({ + url: `${config.API_BASE_URL}/love-points/share`, + method: 'POST', + header: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + success: resolve, + fail: reject + }) + }) + + if (res.data && res.data.success && res.data.data) { + const newShareCount = this.data.shareCount + 1 + const newLovePoints = this.data.lovePoints + (res.data.data.earned || 0) + + this.setData({ + shareCount: newShareCount, + lovePoints: newLovePoints + }) + + console.log('[invite] 分享爱心值奖励:', res.data.data.earned) + } + } catch (err) { + console.error('[invite] 记录分享奖励失败:', err) + } + }, + + /** + * 触发其他页面刷新 + */ + triggerPageRefresh() { + const pages = getCurrentPages() + if (pages.length > 1) { + const prevPage = pages[pages.length - 2] + // 如果上一个页面有刷新方法,调用它 + if (prevPage.onLovePointsUpdate) { + prevPage.onLovePointsUpdate() + } + if (prevPage.loadData) { + prevPage.loadData() + } + } + }, + + /** + * 微信分享配置 + * 用户点击分享按钮时触发,返回分享信息 + * 同时记录爱心值奖励(分享人A获得+100) + */ + onShareAppMessage() { + const userId = wx.getStorageSync('user_id') || '' + const { referralCode, isDistributor, shareConfig } = this.data + const inviterParam = userId ? `?inviter=${userId}` : '' + const referralParam = referralCode ? `?referralCode=${referralCode}` : '' + + // 记录爱心值奖励(分享人A获得+100) + this.recordShareReward() + + if (shareConfig && (userId || (isDistributor && referralCode))) { + return { + title: shareConfig.title, + desc: shareConfig.desc || '', + path: `${shareConfig.path || '/pages/index/index'}${isDistributor && referralCode ? referralParam : inviterParam}`, + imageUrl: shareConfig.imageUrl + } + } + + if (isDistributor && referralCode) { + return { + title: `我的推荐码:${referralCode},注册即可享受优惠!`, + path: `/pages/index/index?referralCode=${referralCode}`, + imageUrl: shareConfig?.imageUrl || '/images/share-cover.jpg' + } + } + + if (userId) { + return { + title: '邀请你一起来玩AI陪伴,超多有趣角色等你来聊~', + path: `/pages/index/index?inviter=${userId}`, + imageUrl: shareConfig?.imageUrl || '/images/share-cover.jpg' + } + } + + return { + title: 'AI情感陪伴,随时可聊 一直陪伴', + path: '/pages/index/index', + imageUrl: shareConfig?.imageUrl || '/images/share-cover.jpg' + } + }, + + /** + * 分享到朋友圈 + */ + onShareTimeline() { + const { referralCode, isDistributor, shareConfig } = this.data + const userId = wx.getStorageSync('user_id') || '' + + this.recordShareReward() + + if (shareConfig && (userId || (isDistributor && referralCode))) { + return { + title: shareConfig.title, + query: isDistributor && referralCode ? `referralCode=${referralCode}` : (userId ? `inviter=${userId}` : ''), + imageUrl: shareConfig.imageUrl + } + } + + if (isDistributor && referralCode) { + return { + title: `推荐码:${referralCode},注册即可享受优惠!`, + query: `referralCode=${referralCode}`, + imageUrl: shareConfig?.imageUrl || '/images/share-cover.jpg' + } + } + + if (userId) { + return { + title: '邀请你一起来玩AI陪伴~', + query: `inviter=${userId}`, + imageUrl: shareConfig?.imageUrl || '/images/share-cover.jpg' + } + } + + return { + title: 'AI情感陪伴', + query: '', + imageUrl: shareConfig?.imageUrl || '/images/share-cover.jpg' + } + }, + + /** + * 加载海报模板 + * 调用后端API获取海报图片 + */ + async loadPosterTemplates() { + try { + const res = await api.pageAssets.getAssets('poster_templates') + + if (res.success && res.data && res.data.length > 0) { + const posterTemplates = res.data.map(item => ({ + id: item.id, + imageUrl: this.processImageUrl(item.asset_url || item.imageUrl || item.url) + })) + this.setData({ + posterTemplates, + currentPosterIndex: 0 + }) + console.log(`加载了 ${posterTemplates.length} 个海报模板`) + } else { + this.setDefaultPosterTemplates() + } + } catch (error) { + console.error('加载海报模板失败:', error) + this.setDefaultPosterTemplates() + } + }, + + /** + * 设置默认海报模板(降级方案 - 使用CDN URL) + */ + setDefaultPosterTemplates() { + const cdnBase = 'https://ai-c.maimanji.com/images' + this.setData({ + posterTemplates: [ + { id: 1, imageUrl: `${cdnBase}/service-banner-1.png` }, + { id: 2, imageUrl: `${cdnBase}/service-banner-2.png` } + ], + currentPosterIndex: 0 + }) + console.log('使用默认海报模板配置') + }, + + /** + * 加载分享配置 + */ + async loadShareConfig() { + try { + const res = await api.promotion.getShareConfig('invite') + if (res.success && res.data) { + const util = require('../../utils/util') + const shareConfig = { + ...res.data, + imageUrl: res.data.imageUrl ? util.getFullImageUrl(res.data.imageUrl) : '' + } + this.setData({ shareConfig }) + } + } catch (error) { + console.error('加载分享配置失败', error) + } + }, + + /** + * 切换海报 + */ + onPosterChange(e) { + this.setData({ + currentPosterIndex: e.detail.current + }) + }, + + /** + * 生成并保存海报 + */ + async generatePoster() { + if (this.data.generatingPoster) return + this.setData({ generatingPoster: true }) + + wx.showLoading({ title: '生成中...' }) + + try { + const template = this.data.posterTemplates[this.data.currentPosterIndex] + const userInfo = wx.getStorageSync('user_info') || {} + const nickname = userInfo.nickname || '神秘用户' + + const query = wx.createSelectorQuery() + query.select('#posterCanvas') + .fields({ node: true, size: true }) + .exec(async (res) => { + if (!res[0]) { + throw new Error('Canvas not found') + } + + const canvas = res[0].node + const ctx = canvas.getContext('2d') + const dpr = wx.getSystemInfoSync().pixelRatio + + canvas.width = res[0].width * dpr + canvas.height = res[0].height * dpr + ctx.scale(dpr, dpr) + + const width = 300 + const height = 533 + + // 绘制背景 + const bgImage = canvas.createImage() + bgImage.src = template.imageUrl + await new Promise((resolve) => { + bgImage.onload = resolve + bgImage.onerror = (e) => { console.error('BG Error', e); resolve() } + }) + ctx.drawImage(bgImage, 0, 0, width, height) + + // 绘制二维码背景 + ctx.fillStyle = '#FFFFFF' + ctx.fillRect(20, height - 120, 100, 100) + + // 模拟二维码 + ctx.fillStyle = '#000000' + ctx.fillRect(25, height - 115, 90, 90) + + // 绘制文字 + ctx.fillStyle = '#FFFFFF' + ctx.font = 'bold 18px sans-serif' + ctx.fillText(nickname, 130, height - 80) + + ctx.font = '14px sans-serif' + ctx.fillText('邀请你体验AI心伴', 130, height - 55) + + wx.canvasToTempFilePath({ + canvas, + success: (fileRes) => { + wx.saveImageToPhotosAlbum({ + filePath: fileRes.tempFilePath, + success: () => { + wx.hideLoading() + wx.showToast({ title: '已保存到相册', icon: 'success' }) + this.setData({ generatingPoster: false }) + }, + fail: (err) => { + console.error('保存失败', err) + wx.hideLoading() + if (err.errMsg.includes('auth')) { + wx.showModal({ + title: '提示', + content: '需要保存图片到相册的权限,是否去设置?', + success: (mRes) => { + if (mRes.confirm) wx.openSetting() + } + }) + } else { + wx.showToast({ title: '保存失败', icon: 'none' }) + } + this.setData({ generatingPoster: false }) + } + }) + }, + fail: (err) => { + console.error('导出图片失败', err) + wx.hideLoading() + this.setData({ generatingPoster: false }) + } + }) + }) + } catch (error) { + console.error('流程错误:', error) + wx.hideLoading() + this.setData({ generatingPoster: false }) + } + }, + + /** + * 查看爱心值明细 + */ + viewTransactions() { + wx.navigateTo({ + url: '/pages/love-transactions/love-transactions' + }) + }, + + /** + * 下拉刷新 + */ + onPullDownRefresh() { + this.loadData().then(() => { + wx.stopPullDownRefresh() + }) + } +}) diff --git a/pages/invite/invite.json b/pages/invite/invite.json new file mode 100644 index 0000000..025fb9f --- /dev/null +++ b/pages/invite/invite.json @@ -0,0 +1,7 @@ +{ + "navigationBarTitleText": "邀请好友", + "navigationBarBackgroundColor": "#A78BFA", + "navigationBarTextStyle": "white", + "enablePullDownRefresh": true, + "backgroundColor": "#A78BFA" +} diff --git a/pages/invite/invite.wxml b/pages/invite/invite.wxml new file mode 100644 index 0000000..2855aec --- /dev/null +++ b/pages/invite/invite.wxml @@ -0,0 +1,107 @@ + + + 邀请好友 + 好友注册并完善资料,你将获得50爱心值 + + + + + + 当前爱心值 + {{lovePoints}} + + + + 今日已获得 + {{shareCount * 20}} + + + + + + + 我的{{isDistributor ? '专属' : ''}}邀请码 + {{isDistributor && referralCode ? referralCode : invitationCode}} + + + + + + + + {{stats.total_invites}} + 邀请人数 + + + {{stats.completed_invites}} + 完成注册 + + + + + + + 活动说明 + + + + + 每次分享获得20爱心值 + + + + 每天最多分享3次,共60爱心值 + + + + 爱心值可用于兑换会员和礼品 + + + + 每日0点重置分享次数 + + + 💰 + 分销商推荐好友消费可获得佣金奖励 + + + + + + + 推广工具 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pages/invite/invite.wxss b/pages/invite/invite.wxss new file mode 100644 index 0000000..de7690e --- /dev/null +++ b/pages/invite/invite.wxss @@ -0,0 +1,488 @@ +.invite-container { + min-height: 100vh; + background: linear-gradient(180deg, #A78BFA 0%, #8B5CF6 100%); + padding: 40rpx; + padding-bottom: 80rpx; +} + +/* 头部 */ +.invite-header { + text-align: center; + color: white; + margin-bottom: 40rpx; + padding: 40rpx 0; +} + +.title { + display: block; + font-size: 56rpx; + font-weight: bold; + margin-bottom: 20rpx; + text-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.2); +} + +.subtitle { + display: block; + font-size: 28rpx; + opacity: 0.9; +} + +/* 爱心值卡片 */ +.love-card { + background: white; + border-radius: 20rpx; + padding: 40rpx; + margin-bottom: 30rpx; + box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1); + display: flex; + align-items: center; +} + +.love-info { + flex: 1; + text-align: center; +} + +.love-label { + display: block; + font-size: 24rpx; + color: #999; + margin-bottom: 10rpx; +} + +.love-value { + display: block; + font-size: 48rpx; + font-weight: bold; + color: #8B5CF6; +} + +.divider { + width: 2rpx; + height: 80rpx; + background: #eee; +} + +/* 进度卡片 */ +.progress-card { + background: white; + border-radius: 20rpx; + padding: 40rpx; + margin-bottom: 30rpx; + box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1); +} + +.progress-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20rpx; +} + +.progress-title { + font-size: 32rpx; + font-weight: bold; + color: #333; +} + +.progress-count { + font-size: 28rpx; + color: #8B5CF6; + font-weight: bold; +} + +.progress-bar { + height: 16rpx; + background: #f0f0f0; + border-radius: 8rpx; + overflow: hidden; + margin-bottom: 20rpx; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #A78BFA 0%, #8B5CF6 100%); + border-radius: 8rpx; + transition: width 0.3s ease; +} + +.progress-tip { + display: block; + font-size: 24rpx; + color: #999; + text-align: center; +} + +/* 推广工具区域 */ +.promotion-section { + background: white; + margin: 30rpx 20rpx; + padding: 30rpx; + border-radius: 20rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.section-title { + font-size: 30rpx; + font-weight: 600; + color: #333; + margin-bottom: 24rpx; +} + +/* 海报生成器 */ +.poster-generator { + margin-bottom: 30rpx; +} + +.poster-swiper { + height: 533rpx; /* Canvas尺寸的一半,或者适配屏幕 */ + margin-bottom: 24rpx; +} + +.poster-item { + display: flex; + justify-content: center; + align-items: center; +} + +.poster-wrap { + width: 90%; + height: 100%; + border-radius: 16rpx; + overflow: hidden; + position: relative; + transition: all 0.3s; + transform: scale(0.95); + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); +} + +.poster-wrap.active { + transform: scale(1); + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15); + border: 4rpx solid #a78bfa; +} + +.poster-img { + width: 100%; + height: 100%; +} + +.poster-overlay { + position: absolute; + top: 10rpx; + right: 10rpx; + background: white; + border-radius: 50%; + width: 40rpx; + height: 40rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.generate-btn { + background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%); + color: white; + font-size: 28rpx; + font-weight: 600; + border-radius: 44rpx; + margin-top: 20rpx; +} + +/* 一键推广按钮 */ +.promote-btn { + background: white; + color: #333; + border: 2rpx solid #a78bfa; + margin-top: 0; + box-shadow: none; +} + +.promote-btn .btn-icon { + width: 36rpx; + height: 36rpx; + margin-right: 10rpx; +} + +/* 按钮基础样式覆盖 */ +.action-btn { + width: 100%; + height: 88rpx; + display: flex; + align-items: center; + justify-content: center; + border-radius: 44rpx; + font-size: 30rpx; + font-weight: 600; + margin-top: 20rpx; +} + +.outline-btn { + background: transparent; + color: #999; + border: 2rpx solid #eee; + box-shadow: none; +} + +/* 分享按钮 */ +.share-btn { + background: linear-gradient(135deg, #A78BFA 0%, #8B5CF6 100%); + color: white; + border-radius: 50rpx; + height: 100rpx; + line-height: 100rpx; + font-size: 32rpx; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 10rpx 30rpx rgba(139, 92, 246, 0.4); + border: none; + margin-bottom: 30rpx; +} + +.share-btn::after { + border: none; +} + +.share-btn.disabled { + background: #ccc; + box-shadow: none; +} + +.btn-icon { + width: 40rpx; + height: 40rpx; + margin-right: 10rpx; +} + +/* 佣金收益卡片 */ +.commission-card { + background: linear-gradient(135deg, #B06AB3 0%, #9B4D9E 100%); + border-radius: 20rpx; + padding: 40rpx; + margin-bottom: 30rpx; + box-shadow: 0 10rpx 30rpx rgba(176, 106, 179, 0.3); +} + +.commission-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30rpx; +} + +.commission-title { + font-size: 32rpx; + font-weight: bold; + color: white; +} + +.commission-badge { + background: rgba(255, 255, 255, 0.2); + color: white; + font-size: 22rpx; + padding: 8rpx 20rpx; + border-radius: 20rpx; + border: 1rpx solid rgba(255, 255, 255, 0.3); +} + +.commission-stats { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30rpx; +} + +.commission-item { + flex: 1; + text-align: center; +} + +.commission-value { + display: block; + font-size: 40rpx; + font-weight: bold; + color: white; + margin-bottom: 10rpx; +} + +.commission-label { + display: block; + font-size: 22rpx; + color: rgba(255, 255, 255, 0.8); +} + +.commission-divider { + width: 2rpx; + height: 60rpx; + background: rgba(255, 255, 255, 0.2); +} + +.commission-btn { + background: white; + color: #9B4D9E; + border-radius: 50rpx; + height: 80rpx; + line-height: 80rpx; + font-size: 28rpx; + font-weight: bold; + border: none; +} + +.commission-btn::after { + border: none; +} + +/* 推荐码区域 */ +.referral-code-section { + background: rgba(255, 255, 255, 0.95); + border-radius: 20rpx; + padding: 40rpx; + margin-bottom: 30rpx; + box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1); + border: 2rpx solid #B06AB3; +} + +/* 邀请码区域 */ +.invite-code-section { + background: rgba(255, 255, 255, 0.95); + border-radius: 20rpx; + padding: 40rpx; + margin-bottom: 30rpx; + box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1); +} + +.code-display { + text-align: center; + margin-bottom: 30rpx; +} + +.label { + display: block; + font-size: 28rpx; + color: #666; + margin-bottom: 20rpx; +} + +.code { + display: block; + font-size: 56rpx; + font-weight: bold; + color: #8B5CF6; + letter-spacing: 8rpx; + font-family: 'Courier New', monospace; +} + +.copy-btn { + background: linear-gradient(135deg, #A78BFA 0%, #8B5CF6 100%); + color: white; + border-radius: 50rpx; + height: 80rpx; + line-height: 80rpx; + font-size: 28rpx; + border: none; +} + +.copy-btn::after { + border: none; +} + +/* 统计区域 */ +.stats-section { + background: rgba(255, 255, 255, 0.95); + border-radius: 20rpx; + padding: 40rpx; + margin-bottom: 30rpx; + display: flex; + box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1); +} + +.stat-item { + flex: 1; + text-align: center; +} + +.stat-item .value { + display: block; + font-size: 48rpx; + font-weight: bold; + color: #8B5CF6; + margin-bottom: 10rpx; +} + +.stat-item .label { + display: block; + font-size: 24rpx; + color: #999; +} + +/* 说明卡片 */ +.tips-card { + background: rgba(255, 255, 255, 0.95); + border-radius: 20rpx; + padding: 40rpx; + margin-bottom: 30rpx; + box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1); +} + +.tips-title { + font-size: 32rpx; + font-weight: bold; + color: #333; + margin-bottom: 30rpx; +} + +.tips-list { + padding-left: 10rpx; +} + +.tip-item { + display: flex; + margin-bottom: 20rpx; + line-height: 40rpx; +} + +.tip-dot { + color: #8B5CF6; + margin-right: 10rpx; + font-size: 32rpx; +} + +.tip-text { + flex: 1; + font-size: 26rpx; + color: #666; +} + +.tip-item.highlight { + background: rgba(176, 106, 179, 0.1); + padding: 15rpx; + border-radius: 10rpx; + margin-top: 10rpx; +} + +.tip-item.highlight .tip-text { + color: #9B4D9E; + font-weight: 500; +} + +/* 操作区域 */ +.action-section { + margin-top: 30rpx; +} + +.action-btn { + background: rgba(255, 255, 255, 0.95); + color: #8B5CF6; + border-radius: 50rpx; + height: 80rpx; + line-height: 80rpx; + font-size: 28rpx; + font-weight: bold; + border: 2rpx solid #8B5CF6; +} + +.action-btn::after { + border: none; +} diff --git a/pages/login/login.js b/pages/login/login.js new file mode 100644 index 0000000..592d0d4 --- /dev/null +++ b/pages/login/login.js @@ -0,0 +1,236 @@ +/** + * 登录页面 + * 显示LOGO、应用名称和微信手机号快速登录按钮 + * 用户需勾选同意协议后才能登录 + * + * 支持持久化登录: + * - 登录成功后保存Token到本地(7天有效期) + * - 再次进入时自动恢复登录状态 + */ +const app = getApp() +const auth = require('../../utils/auth') +const api = require('../../utils/api') +const config = require('../../config/index') + +Page({ + data: { + loginLoading: false, + statusBarHeight: 20, + agreementChecked: false // 协议默认不勾选 + }, + + onLoad(options) { + // 保存来源页面,登录成功后跳转 + this.redirectUrl = options.redirect || '' + + // 获取状态栏高度 + const systemInfo = wx.getSystemInfoSync() + this.setData({ + statusBarHeight: systemInfo.statusBarHeight || 20 + }) + + // 检查是否已登录,如果已登录则直接跳转 + this.checkExistingLogin() + }, + + /** + * 检查现有登录状态 + * 如果已登录且Token有效,直接跳转 + */ + async checkExistingLogin() { + if (auth.isLoggedIn()) { + // 验证服务端Token + const result = await auth.verifyLogin() + if (result.valid) { + console.log('已登录,直接跳转') + this.navigateAfterLogin() + } + } + }, + + /** + * 切换协议勾选状态 + */ + toggleAgreement() { + this.setData({ + agreementChecked: !this.data.agreementChecked + }) + }, + + /** + * 未勾选协议时点击登录按钮 + */ + onLoginBtnTap() { + if (!this.data.agreementChecked) { + wx.showToast({ + title: '请先同意用户协议和隐私协议', + icon: 'none', + duration: 2000 + }) + } + }, + + /** + * 显示用户服务协议 + */ + showUserAgreement() { + wx.navigateTo({ + url: '/pages/agreement/agreement?code=user_service' + }) + }, + + /** + * 显示隐私协议 + */ + showPrivacyPolicy() { + wx.navigateTo({ + url: '/pages/agreement/agreement?code=privacy_policy' + }) + }, + + /** + * 微信手机号快速登录 + * @param {object} e - 事件对象 + */ + async onGetPhoneNumber(e) { + console.log('手机号授权回调', e.detail) + + // 用户取消授权 + if (e.detail.errMsg !== 'getPhoneNumber:ok') { + wx.showToast({ + title: '您已取消授权', + icon: 'none', + duration: 2000 + }) + return + } + + // 获取手机号授权code + const phoneCode = e.detail.code + if (!phoneCode) { + wx.showModal({ + title: '提示', + content: '获取授权信息失败,请重试', + showCancel: false + }) + return + } + + // 显示加载状态 + this.setData({ loginLoading: true }) + wx.showLoading({ title: '登录中...', mask: true }) + + try { + // 1. 获取登录code (用于换取openid) + const loginRes = await new Promise((resolve, reject) => { + wx.login({ + success: resolve, + fail: reject + }) + }) + + if (!loginRes.code) { + throw new Error('获取登录凭证失败') + } + + // 2. 使用app的wxPhoneLogin方法完成登录 (传递两个code) + await app.wxPhoneLogin(phoneCode, loginRes.code) + + wx.hideLoading() + wx.showToast({ + title: '登录成功', + icon: 'success', + duration: 1500 + }) + + // 登录成功后调用爱心值API(B自己获得+100) + this.claimLovePointsLoginReward() + + // 延迟跳转,让用户看到成功提示 + setTimeout(() => { + this.navigateAfterLogin() + }, 1500) + + } catch (err) { + console.error('手机号登录失败', err) + wx.hideLoading() + this.handleLoginError(err) + } finally { + this.setData({ loginLoading: false }) + } + }, + + /** + * 登录成功后跳转 + */ + navigateAfterLogin() { + if (this.redirectUrl) { + // 跳转到来源页面 + wx.redirectTo({ + url: decodeURIComponent(this.redirectUrl), + fail: () => { + // 如果是tabBar页面,使用switchTab + wx.switchTab({ + url: decodeURIComponent(this.redirectUrl).split('?')[0] + }) + } + }) + } else { + // 默认跳转到首页 + wx.switchTab({ + url: '/pages/index/index' + }) + } + }, + + /** + * 处理登录错误 + * @param {object} err - 错误对象 + */ + handleLoginError(err) { + let message = '登录失败,请稍后重试' + + if (err.code === 'PHONE_DECRYPT_FAILED') { + message = '手机号解密失败,请重试' + } else if (err.code === 'WX_SESSION_EXPIRED') { + message = '微信会话已过期,请重新打开小程序' + } else if (err.message) { + message = err.message + } + + wx.showModal({ + title: '登录失败', + content: message, + showCancel: false + }) + }, + + /** + * 返回上一页 + */ + goBack() { + const pages = getCurrentPages() + if (pages.length > 1) { + wx.navigateBack() + } else { + wx.switchTab({ + url: '/pages/index/index' + }) + } + }, + + /** + * 领取登录爱心值奖励(B自己获得+100) + * 后端会同时检查是否有邀请人,如果有则给邀请人也+100 + */ + async claimLovePointsLoginReward() { + try { + const res = await api.lovePoints.login() + if (res.success && res.data && res.data.earned > 0) { + console.log('[login] 登录爱心值奖励:', res.data.earned) + } + } catch (err) { + console.error('[login] 领取登录爱心值失败:', err) + } + } +}) diff --git a/pages/login/login.json b/pages/login/login.json new file mode 100644 index 0000000..089f946 --- /dev/null +++ b/pages/login/login.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "", + "navigationStyle": "custom", + "navigationBarBackgroundColor": "#E8C3D4" +} diff --git a/pages/login/login.wxml b/pages/login/login.wxml new file mode 100644 index 0000000..ae71821 --- /dev/null +++ b/pages/login/login.wxml @@ -0,0 +1,56 @@ + + diff --git a/pages/login/login.wxss b/pages/login/login.wxss new file mode 100644 index 0000000..4c71b79 --- /dev/null +++ b/pages/login/login.wxss @@ -0,0 +1,194 @@ +/* 登录页面样式 */ +.login-page { + min-height: 100vh; + background: linear-gradient(180deg, #E8C3D4 0%, #f5e6ed 50%, #fff 100%); + position: relative; + overflow: hidden; +} + +/* 背景装饰 */ +.bg-decoration { + position: absolute; + top: -200rpx; + right: -200rpx; + width: 600rpx; + height: 600rpx; + background: radial-gradient(circle, rgba(145, 69, 132, 0.1) 0%, transparent 70%); + border-radius: 50%; +} + +/* 导航栏 */ +.nav-bar { + position: relative; + height: 88rpx; + display: flex; + align-items: center; + padding: 0 32rpx; + margin-top: 88rpx; +} + +.back-btn { + width: 72rpx; + height: 72rpx; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.6); + border-radius: 50%; +} + +.back-icon { + width: 40rpx; + height: 40rpx; +} + +/* 主内容区 */ +.content { + display: flex; + flex-direction: column; + align-items: center; + padding: 120rpx 64rpx 80rpx; +} + +/* Logo区域 */ +.logo-section { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 160rpx; +} + +.logo-wrapper { + margin-bottom: 48rpx; +} + +.logo-circle { + width: 200rpx; + height: 200rpx; + background: linear-gradient(135deg, #914584 0%, #B378FE 100%); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 20rpx 60rpx rgba(145, 69, 132, 0.3); +} + +.logo-icon { + width: 100rpx; + height: 100rpx; + filter: brightness(0) invert(1); +} + +.app-name { + font-size: 64rpx; + font-weight: bold; + color: #914584; + margin-bottom: 16rpx; + letter-spacing: 8rpx; +} + +.app-slogan { + font-size: 28rpx; + color: #717182; +} + +/* 登录按钮区域 */ +.login-section { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; +} + +/* 协议勾选区域 */ +.agreement-section { + display: flex; + align-items: flex-start; + margin-bottom: 40rpx; + width: 100%; +} + +.checkbox-wrap { + padding: 8rpx; + margin-right: 12rpx; +} + +.checkbox { + width: 40rpx; + height: 40rpx; + border: 3rpx solid #d1d5db; + border-radius: 8rpx; + display: flex; + align-items: center; + justify-content: center; + background: #fff; + transition: all 0.2s ease; +} + +.checkbox.checked { + background: linear-gradient(135deg, #914584 0%, #B378FE 100%); + border-color: #914584; +} + +.check-icon { + width: 28rpx; + height: 28rpx; + filter: brightness(0) invert(1); +} + +.agreement-text { + flex: 1; + display: flex; + flex-wrap: wrap; + line-height: 48rpx; +} + +.agreement-label { + font-size: 26rpx; + color: #6b7280; +} + +.agreement-link { + font-size: 26rpx; + color: #914584; + font-weight: 500; +} + +.login-btn { + width: 100%; + height: 100rpx; + background: linear-gradient(135deg, #07C160 0%, #2AAE67 100%); + border-radius: 50rpx; + display: flex; + align-items: center; + justify-content: center; + gap: 16rpx; + border: none; + box-shadow: 0 16rpx 40rpx rgba(7, 193, 96, 0.3); +} + +.login-btn::after { + border: none; +} + +.login-btn.loading { + opacity: 0.7; +} + +.login-btn.disabled { + opacity: 0.5; + box-shadow: none; +} + +.btn-icon { + width: 44rpx; + height: 44rpx; +} + +.btn-text { + font-size: 34rpx; + font-weight: bold; + color: #fff; + letter-spacing: 2rpx; + white-space: nowrap; +} diff --git a/pages/love-transactions/love-transactions.js b/pages/love-transactions/love-transactions.js new file mode 100644 index 0000000..e233ee6 --- /dev/null +++ b/pages/love-transactions/love-transactions.js @@ -0,0 +1,143 @@ +const api = require('../../utils/api') +const auth = require('../../utils/auth') + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + transactions: [], + balance: 0, + loading: false + }, + + async onLoad() { + // 计算导航栏高度 + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight + }) + + // 统一登录验证 + const isValid = await auth.ensureLogin({ + pageName: 'love-transactions', + redirectUrl: '/pages/love-transactions/love-transactions' + }) + + if (!isValid) return + + // 验证通过后,稍作延迟确保token稳定 + await new Promise(resolve => setTimeout(resolve, 50)) + + // 加载数据 + this.loadBalance() + this.loadTransactions() + }, + + onShow() { + // 每次显示页面时刷新数据(已登录的情况下) + const app = getApp() + if (app.globalData.isLoggedIn) { + this.loadBalance() + this.loadTransactions() + } + }, + + async loadBalance() { + try { + const res = await api.loveExchange.getOptions() + if (res.success) { + this.setData({ + balance: res.data.current_love_points || 0 + }) + } + } catch (error) { + console.error('加载余额失败:', error) + // 401错误由API层统一处理 + } + }, + + async loadTransactions() { + if (this.data.loading) return + + this.setData({ loading: true }) + + try { + const res = await api.lovePoints.getTransactions({ limit: 100 }) + if (res.success) { + this.setData({ + transactions: res.data + }) + } + } catch (error) { + console.error('加载流水记录失败:', error) + // 401错误由API层统一处理,这里只处理其他错误 + if (error.code !== 401) { + wx.showToast({ + title: error.message || '加载失败', + icon: 'none' + }) + } + } finally { + this.setData({ loading: false }) + } + }, + + // 返回上一页 + goBack() { + wx.navigateBack() + }, + + // 格式化来源 + formatSource(source) { + const sourceMap = { + 'share': '分享小程序', + 'invite': '邀请好友', + 'profile': '完善资料', + 'exchange_vip': '兑换会员', + 'exchange_gift': '兑换礼品', + 'admin': '管理员操作', + 'system': '系统赠送' + } + return sourceMap[source] || source + }, + + // 格式化时间 + formatTime(dateStr) { + const date = new Date(dateStr) + const now = new Date() + const diff = now - date + + // 今天 + if (diff < 86400000 && date.getDate() === now.getDate()) { + return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) + } + + // 昨天 + const yesterday = new Date(now) + yesterday.setDate(yesterday.getDate() - 1) + if (date.getDate() === yesterday.getDate()) { + return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) + } + + // 其他日期 + return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }) + ' ' + + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) + }, + + // 下拉刷新 + onPullDownRefresh() { + this.loadBalance() + this.loadTransactions() + setTimeout(() => { + wx.stopPullDownRefresh() + }, 1000) + } +}) diff --git a/pages/love-transactions/love-transactions.json b/pages/love-transactions/love-transactions.json new file mode 100644 index 0000000..2879ab0 --- /dev/null +++ b/pages/love-transactions/love-transactions.json @@ -0,0 +1,5 @@ +{ + "navigationStyle": "custom", + "enablePullDownRefresh": true, + "backgroundColor": "#F8F9FA" +} diff --git a/pages/love-transactions/love-transactions.wxml b/pages/love-transactions/love-transactions.wxml new file mode 100644 index 0000000..b2ebcaf --- /dev/null +++ b/pages/love-transactions/love-transactions.wxml @@ -0,0 +1,69 @@ + + + + + + + + + + 爱心值明细 + + + + + + + + + + + + + + + + + + + + 我的爱心 + + {{balance}} + + + + + + + + + + + + + 明细记录 + + + + + + + {{item.description || formatSource(item.source)}} + {{formatTime(item.created_at)}} + + + + {{item.amount > 0 ? '+' : ''}}{{item.amount}} + + + + + + + + 暂无记录 + + + + diff --git a/pages/love-transactions/love-transactions.wxss b/pages/love-transactions/love-transactions.wxss new file mode 100644 index 0000000..0ea5af6 --- /dev/null +++ b/pages/love-transactions/love-transactions.wxss @@ -0,0 +1,246 @@ +/* 页面容器 */ +.page-container { + min-height: 100vh; + background: #F8F9FA; +} + +/* 固定导航栏容器 */ +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; + background: rgba(242, 237, 255, 0.6); + backdrop-filter: blur(10px); +} + +.status-bar { + background: transparent; +} + +.nav-bar { + display: flex; + align-items: center; + justify-content: center; + padding: 0 32rpx; + background: transparent; + position: relative; +} + +.nav-back { + position: absolute; + left: 32rpx; + top: 50%; + transform: translateY(-50%); + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.back-icon { + width: 48rpx; + height: 48rpx; +} + +.nav-title { + font-size: 36rpx; + font-weight: 700; + color: #1F2937; + line-height: 1; +} + +/* 内容滚动区域 */ +.content-scroll { + height: 100vh; + box-sizing: border-box; +} + +/* 爱心值卡片 */ +.love-card { + margin: 32rpx 32rpx 40rpx; + border-radius: 24rpx; + overflow: hidden; + position: relative; + height: 280rpx; +} + +.love-card-bg { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, #A78BFA 0%, #C084FC 50%, #E879F9 100%); + opacity: 1; +} + +.love-card-gradient { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.2) 0%, transparent 50%); +} + +.love-card-content { + position: relative; + padding: 40rpx; + display: flex; + justify-content: space-between; + align-items: center; + height: 100%; +} + +/* 左侧信息 */ +.love-info { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; +} + +.love-header { + display: flex; + align-items: center; + margin-bottom: 16rpx; +} + +.love-icon-wrap { + width: 48rpx; + height: 48rpx; + background: rgba(255, 255, 255, 0.3); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 12rpx; +} + +.love-icon { + width: 28rpx; + height: 28rpx; +} + +.love-label { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.95); + font-weight: 500; +} + +.love-value { + font-size: 72rpx; + font-weight: 700; + color: #FFFFFF; + line-height: 1.2; + letter-spacing: 1rpx; +} + +/* 右侧装饰 */ +.love-decoration { + width: 160rpx; + height: 160rpx; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.3; +} + +.decoration-icon { + width: 120rpx; + height: 120rpx; +} + +/* 明细记录区域 */ +.records-section { + margin: 0 32rpx 40rpx; +} + +.records-header { + margin-bottom: 24rpx; +} + +.records-title { + font-size: 32rpx; + font-weight: 600; + color: #1F2937; +} + +/* 记录列表 */ +.records-list { + background: #FFFFFF; + border-radius: 16rpx; + overflow: hidden; +} + +.record-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 32rpx; + border-bottom: 1rpx solid #F3F4F6; +} + +.record-item:last-child { + border-bottom: none; +} + +.record-left { + flex: 1; + display: flex; + flex-direction: column; +} + +.record-title { + font-size: 28rpx; + color: #1F2937; + font-weight: 500; + margin-bottom: 8rpx; +} + +.record-time { + font-size: 24rpx; + color: #9CA3AF; +} + +.record-right { + margin-left: 24rpx; +} + +.record-amount { + font-size: 32rpx; + font-weight: 600; +} + +.record-amount.add { + color: #10B981; +} + +/* .record-amount.add::before { + content: '+'; +} */ + +.record-amount.minus { + color: #EF4444; +} + +/* .record-amount.minus::before { + content: '-'; +} */ + +/* 空状态 */ +.empty-state { + background: #FFFFFF; + border-radius: 16rpx; + padding: 120rpx 0; + text-align: center; +} + +.empty-text { + font-size: 28rpx; + color: #9CA3AF; +} diff --git a/pages/medical-apply/medical-apply.js b/pages/medical-apply/medical-apply.js new file mode 100644 index 0000000..12c6a57 --- /dev/null +++ b/pages/medical-apply/medical-apply.js @@ -0,0 +1,315 @@ +// pages/medical-apply/medical-apply.js +// 陪诊就医申请页面 +const api = require('../../utils/api') + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + showForm: true, + applyStatus: 'none', // none, pending, approved, rejected + statusTitle: '', + statusDesc: '', + isReapply: false, + agreed: false, + formData: { + avatar: '', + realName: '', + gender: '', + age: '', + idCard: '', + city: '', + hospital: '', + healthCert: '', + idFront: '', + idBack: '', + otherCert: '', + introduction: '', + phone: '', + emergencyContact: '', + emergencyPhone: '' + }, + canSubmit: false + }, + + onLoad(options) { + // 获取系统信息设置导航栏高度 + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight, + isReapply: options.isReapply === 'true' + }) + + this.checkApplyStatus() + }, + + // 返回上一页 + goBack() { + wx.navigateBack() + }, + + // 检查申请状态 + async checkApplyStatus() { + const token = wx.getStorageSync('auth_token') + if (!token) { + this.setData({ applyStatus: 'none' }) + return + } + + wx.showLoading({ title: '加载中...' }) + try { + // 调用陪诊就医申请状态API(如果后端已实现) + const res = await api.request('/medical/apply') + + if (res.success && res.data) { + const data = res.data + if (data.status === 'approved') { + this.setData({ + applyStatus: 'approved', + statusTitle: '申请已通过', + statusDesc: '恭喜您成为陪诊师!' + }) + } else if (data.status === 'pending') { + this.setData({ + applyStatus: 'pending', + statusTitle: '审核中', + statusDesc: '您的申请正在审核中,请耐心等待' + }) + } else if (data.status === 'rejected') { + this.setData({ + applyStatus: 'rejected', + statusTitle: '申请未通过', + statusDesc: data.rejectReason || '很抱歉,您的申请未通过审核' + }) + } + } + } catch (err) { + console.log('获取申请状态失败,可能API未实现:', err) + this.setData({ applyStatus: 'none' }) + } finally { + wx.hideLoading() + } + }, + + // 重新申请 + reapply() { + this.setData({ + isReapply: true, + applyStatus: 'none' + }) + }, + + // 选择头像 + chooseAvatar() { + this.doChooseMedia('avatar') + }, + + // 上传证书 + uploadCert(e) { + const type = e.currentTarget.dataset.type + this.doChooseMedia(type) + }, + + // 选择媒体文件 + doChooseMedia(field) { + wx.chooseMedia({ + count: 1, + mediaType: ['image'], + sourceType: ['album', 'camera'], + success: async (res) => { + const tempFilePath = res.tempFiles[0].tempFilePath + wx.showLoading({ title: '上传中...' }) + + try { + // 1. 自动压缩图片 + const compressed = await wx.compressImage({ + src: tempFilePath, + quality: 80 + }).catch(err => { + console.warn('压缩图片失败:', err) + return { tempFilePath } + }) + + // 2. 上传到服务器,使用允许的目录 'image' + const uploadRes = await api.uploadFile(compressed.tempFilePath, 'image') + + if (uploadRes.success && uploadRes.data) { + const fieldMap = { + 'avatar': 'formData.avatar', + 'idFront': 'formData.idFront', + 'idBack': 'formData.idBack', + 'healthCert': 'formData.healthCert', + 'otherCert': 'formData.otherCert' + } + + this.setData({ + [fieldMap[field]]: uploadRes.data.url + }) + this.checkCanSubmit() + + wx.showToast({ title: '上传成功', icon: 'success' }) + } else { + wx.showToast({ + title: uploadRes.message || '上传失败', + icon: 'none' + }) + } + } catch (err) { + console.error('上传过程出错:', err) + wx.showToast({ + title: err.message || '上传出错', + icon: 'none' + }) + } finally { + wx.hideLoading() + } + } + }) + }, + + // 输入变化 + onInputChange(e) { + const field = e.currentTarget.dataset.field + const value = e.detail.value + this.setData({ + [`formData.${field}`]: value + }) + this.checkCanSubmit() + }, + + // 选择性别 + selectGender(e) { + const gender = e.currentTarget.dataset.gender + this.setData({ + 'formData.gender': gender + }) + this.checkCanSubmit() + }, + + // 切换协议同意状态 + toggleAgreement() { + this.setData({ + agreed: !this.data.agreed + }) + this.checkCanSubmit() + }, + + // 查看协议 + viewAgreement() { + wx.navigateTo({ + url: '/pages/agreement/agreement?code=medical_service' + }) + }, + + // 检查是否可以提交 + checkCanSubmit() { + const { formData, agreed } = this.data + + // 验证身份证号格式 + const idCardValid = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(formData.idCard) + + const canSubmit = + formData.realName && + formData.gender && + formData.age && + formData.idCard && idCardValid && + formData.city && + formData.hospital && + formData.healthCert && + formData.idFront && + formData.idBack && + formData.introduction && + formData.introduction.length >= 50 && + formData.phone && + formData.phone.length === 11 && + agreed + + this.setData({ canSubmit }) + }, + + // 提交申请 + async submitApply() { + if (!this.data.canSubmit) return + + const { formData } = this.data + + // 验证手机号 + if (!/^1[3-9]\d{9}$/.test(formData.phone)) { + wx.showToast({ title: '请输入正确的手机号', icon: 'none' }) + return + } + + // 验证年龄 + const age = parseInt(formData.age) + if (age < 18 || age > 60) { + wx.showToast({ title: '年龄需在18-60岁之间', icon: 'none' }) + return + } + + // 验证身份证号 + if (!/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(formData.idCard)) { + wx.showToast({ title: '请输入正确的身份证号', icon: 'none' }) + return + } + + wx.showLoading({ title: '提交中...' }) + try { + const res = await api.request('/medical/apply', { + method: 'POST', + data: { + avatar: formData.avatar, + realName: formData.realName, + gender: formData.gender, + age: age, + idCard: formData.idCard, + city: formData.city, + hospital: formData.hospital, + healthCert: formData.healthCert, + idFront: formData.idFront, + idBack: formData.idBack, + otherCert: formData.otherCert, + introduction: formData.introduction, + phone: formData.phone, + emergencyContact: formData.emergencyContact, + emergencyPhone: formData.emergencyPhone + } + }) + + if (res.success || res.code === 0) { + wx.showToast({ title: '申请已提交', icon: 'success' }) + this.setData({ + applyStatus: 'pending', + statusTitle: '审核中', + statusDesc: '您的申请正在审核中,请耐心等待', + isReapply: false + }) + } else { + wx.showToast({ title: res.message || '提交失败', icon: 'none' }) + } + } catch (err) { + console.error('提交申请失败:', err) + // 如果API未实现,显示提示 + if (err.code === 404 || err.message?.includes('not found')) { + wx.showModal({ + title: '提示', + content: '陪诊就医服务即将开放,敬请期待!', + showCancel: false, + confirmText: '我知道了', + confirmColor: '#b06ab3' + }) + } else { + wx.showToast({ title: err.message || '提交失败', icon: 'none' }) + } + } finally { + wx.hideLoading() + } + } +}) diff --git a/pages/medical-apply/medical-apply.json b/pages/medical-apply/medical-apply.json new file mode 100644 index 0000000..a3e86ce --- /dev/null +++ b/pages/medical-apply/medical-apply.json @@ -0,0 +1,5 @@ +{ + "usingComponents": {}, + "navigationStyle": "custom", + "navigationBarTextStyle": "black" +} diff --git a/pages/medical-apply/medical-apply.wxml b/pages/medical-apply/medical-apply.wxml new file mode 100644 index 0000000..614aa2d --- /dev/null +++ b/pages/medical-apply/medical-apply.wxml @@ -0,0 +1,267 @@ + + + + + + + + 返回 + + 陪诊就医 + + + + + + + + + + + + + + + + {{statusTitle}} + {{statusDesc}} + + + + + + + + + + 个人照片 + * + + + + + + + 上传照片 + + + + 请上传清晰的个人照片 + + + + + + 基本信息 + + + + + 姓名 + * + + + + + + + + + 性别 + * + + + + + + + + + + + + + + 年龄 + * + + + + + + + + + 身份证号 + * + + + + + + + + + + + 服务区域 + + + + + 所在城市 + * + + + + + + + + + 服务医院 + * + + + + + 可填写多个医院,用逗号分隔 + + + + + + + 资质证书 + + + + + 健康证 + * + + + + + + 上传健康证 + + + + + + + 身份证正面 + * + + + + + + 上传身份证正面 + + + + + + + 身份证反面 + * + + + + + + 上传身份证反面 + + + + + + + 其他资质证书 + + + + + + 上传其他证书(选填) + + + 如护理证、急救证等相关资质 + + + + + + + 个人介绍 + * + + + + + + {{formData.introduction.length || 0}}/500 + + + + + + + 联系方式 + + + + + 手机号 + * + + + + + + + + + 紧急联系人 + + + + + + + + + 紧急联系电话 + + + + + + + + + + + + + + 我已阅读并同意 + 《陪诊就医服务协议》 + + + + + + + + + + + + diff --git a/pages/medical-apply/medical-apply.wxss b/pages/medical-apply/medical-apply.wxss new file mode 100644 index 0000000..a6b18e3 --- /dev/null +++ b/pages/medical-apply/medical-apply.wxss @@ -0,0 +1,374 @@ +/* 陪诊就医申请页面样式 */ + +.page-container { + min-height: 100vh; + background: linear-gradient(180deg, #E8C3D4 0%, #F5E6EC 100%); +} + +/* 导航栏 */ +.nav-header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + background: linear-gradient(135deg, #E8C3D4 0%, #D4A5C9 100%); +} + +.nav-content { + display: flex; + align-items: center; + justify-content: space-between; + height: 44px; + padding: 0 16px; +} + +.nav-back { + display: flex; + align-items: center; + min-width: 60px; +} + +.back-icon { + width: 20px; + height: 20px; +} + +.back-text { + font-size: 14px; + color: #333; + margin-left: 4px; +} + +.nav-title { + font-size: 17px; + font-weight: 600; + color: #333; +} + +.nav-placeholder { + min-width: 60px; +} + +/* 内容滚动区域 */ +.content-scroll { + height: 100vh; + padding-bottom: env(safe-area-inset-bottom); +} + +/* 申请表单样式 */ +.apply-form { + padding: 16px; +} + +/* 状态卡片 */ +.status-card { + background: #fff; + border-radius: 16px; + padding: 40px 24px; + text-align: center; + margin-bottom: 16px; +} + +.status-icon { + width: 80px; + height: 80px; + margin: 0 auto 16px; +} + +.status-icon image { + width: 100%; + height: 100%; +} + +.status-title { + display: block; + font-size: 18px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.status-desc { + font-size: 14px; + color: #666; + line-height: 1.6; +} + +.btn-secondary { + margin-top: 24px; + width: 160px; + height: 44px; + background: #fff; + border: 1px solid #b06ab3; + border-radius: 22px; + color: #b06ab3; + font-size: 15px; +} + +/* 表单内容 */ +.form-content { + background: #fff; + border-radius: 16px; + padding: 24px 20px; +} + +.form-header { + text-align: center; + margin-bottom: 24px; +} + +.form-title { + display: block; + font-size: 20px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.form-subtitle { + font-size: 14px; + color: #999; +} + +/* 表单区块 */ +.form-section { + margin-bottom: 24px; +} + +.section-header { + display: flex; + align-items: center; + margin-bottom: 16px; +} + +.section-title { + font-size: 16px; + font-weight: 600; + color: #333; +} + +.required { + color: #ff4d4f; + margin-left: 4px; +} + +/* 头像上传 */ +.avatar-upload-area { + display: flex; + justify-content: center; + margin-bottom: 8px; +} + +.avatar-circle { + width: 100px; + height: 100px; + border-radius: 50%; + background: #f5f5f5; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; +} + +.avatar-image { + width: 100%; + height: 100%; +} + +.upload-placeholder { + text-align: center; +} + +.camera-icon { + width: 32px; + height: 32px; + margin-bottom: 4px; +} + +.upload-text { + font-size: 12px; + color: #999; +} + +.form-tip { + font-size: 12px; + color: #999; + text-align: center; + margin-top: 8px; +} + +/* 表单项 */ +.form-item { + margin-bottom: 16px; +} + +.item-label-row { + display: flex; + align-items: center; + margin-bottom: 8px; +} + +.item-label { + font-size: 14px; + color: #333; +} + +.input-wrapper { + background: #f8f8f8; + border-radius: 8px; + padding: 0 12px; +} + +.item-input { + width: 100%; + height: 44px; + font-size: 14px; + color: #333; +} + +.placeholder { + color: #ccc; +} + +/* 性别选择 */ +.gender-options { + display: flex; + gap: 12px; +} + +.gender-btn { + flex: 1; + height: 44px; + background: #f8f8f8; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + color: #666; + transition: all 0.3s; +} + +.gender-btn.active { + background: linear-gradient(135deg, #E8C3D4 0%, #D4A5C9 100%); + color: #333; + font-weight: 500; +} + +/* 证书上传 */ +.cert-upload { + width: 100%; + height: 120px; + background: #f8f8f8; + border-radius: 8px; + border: 1px dashed #ddd; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.cert-image { + width: 100%; + height: 100%; +} + +.cert-placeholder { + text-align: center; +} + +.upload-icon { + width: 40px; + height: 40px; + margin-bottom: 8px; +} + +/* 文本域 */ +.textarea-wrapper { + background: #f8f8f8; + border-radius: 8px; + padding: 12px; +} + +.intro-textarea { + width: 100%; + height: 120px; + font-size: 14px; + color: #333; + line-height: 1.6; +} + +.textarea-footer { + display: flex; + justify-content: flex-end; + margin-top: 8px; +} + +.char-count { + font-size: 12px; + color: #999; +} + +/* 协议 */ +.agreement-row { + display: flex; + align-items: center; + margin: 24px 0; +} + +.checkbox { + width: 20px; + height: 20px; + border: 1px solid #ddd; + border-radius: 4px; + margin-right: 8px; + display: flex; + align-items: center; + justify-content: center; +} + +.checkbox.checked { + background: linear-gradient(135deg, #b06ab3 0%, #4568dc 100%); + border-color: transparent; +} + +.check-icon { + width: 14px; + height: 14px; +} + +.agreement-text { + flex: 1; +} + +.normal-text { + font-size: 13px; + color: #666; +} + +.link-text { + font-size: 13px; + color: #b06ab3; +} + +/* 提交按钮 */ +.submit-btn { + width: 100%; + height: 48px; + background: linear-gradient(135deg, #b06ab3 0%, #4568dc 100%); + border-radius: 24px; + color: #fff; + font-size: 16px; + font-weight: 500; + border: none; +} + +.submit-btn.disabled { + opacity: 0.5; +} + +/* 底部占位 */ +.bottom-placeholder { + height: 40px; +} diff --git a/pages/my-activities/my-activities.js b/pages/my-activities/my-activities.js new file mode 100644 index 0000000..ceea4eb --- /dev/null +++ b/pages/my-activities/my-activities.js @@ -0,0 +1,118 @@ +const api = require('../../utils/api'); +const util = require('../../utils/util'); + +Page({ + data: { + activeTab: 'upcoming', // upcoming | ended + activities: [], + loading: false, + hasMore: true, + page: 1, + limit: 20, + statusBarHeight: 20 + }, + + onLoad() { + this.setData({ + statusBarHeight: wx.getSystemInfoSync().statusBarHeight + }); + this.loadActivities(true); + }, + + onPullDownRefresh() { + this.loadActivities(true).then(() => { + wx.stopPullDownRefresh(); + }); + }, + + onReachBottom() { + if (this.data.hasMore && !this.data.loading) { + this.loadActivities(false); + } + }, + + switchTab(e) { + const tab = e.currentTarget.dataset.tab; + if (tab === this.data.activeTab) return; + + this.setData({ + activeTab: tab, + activities: [], + page: 1, + hasMore: true + }, () => { + this.loadActivities(true); + }); + }, + + async loadActivities(isRefresh = false) { + if (this.data.loading) return; + + this.setData({ loading: true }); + if (isRefresh) { + this.setData({ page: 1, hasMore: true }); + } + + try { + // 映射 tab 到 API 的 time_status + const timeStatusMap = { + 'upcoming': 'upcoming', + 'ended': 'finished' + }; + + const res = await api.activity.getMyRegistrations({ + page: this.data.page, + limit: this.data.limit, + time_status: timeStatusMap[this.data.activeTab] + }); + + if (res.success && res.data) { + const newList = res.data.list.map(item => { + // 格式化日期和图片 + return { + ...item, + cover_image: util.getFullImageUrl(item.cover_image || item.coverImage), + date_display: item.start_date || item.activityDate, + // 映射时间状态文字 + time_status_text: this.getTimeStatusText(item.activity_time_status), + time_status_class: item.activity_time_status + }; + }); + + this.setData({ + activities: isRefresh ? newList : [...this.data.activities, ...newList], + page: this.data.page + 1, + hasMore: newList.length === this.data.limit + }); + } + } catch (err) { + console.error('Load my activities failed', err); + wx.showToast({ + title: '加载失败', + icon: 'none' + }); + } finally { + this.setData({ loading: false }); + } + }, + + getTimeStatusText(status) { + const map = { + 'pending': '待开始', + 'started': '进行中', + 'finished': '已结束' + }; + return map[status] || ''; + }, + + goDetail(e) { + const id = e.currentTarget.dataset.id; + wx.navigateTo({ + url: `/pages/activity-detail/activity-detail?id=${id}` + }); + }, + + goBack() { + wx.navigateBack(); + } +}); diff --git a/pages/my-activities/my-activities.json b/pages/my-activities/my-activities.json new file mode 100644 index 0000000..3956bc6 --- /dev/null +++ b/pages/my-activities/my-activities.json @@ -0,0 +1,8 @@ +{ + "usingComponents": { + "app-icon": "/components/icon/icon" + }, + "navigationBarTitleText": "我的活动", + "enablePullDownRefresh": true, + "backgroundColor": "#F9FAFB" +} diff --git a/pages/my-activities/my-activities.wxml b/pages/my-activities/my-activities.wxml new file mode 100644 index 0000000..e163fbf --- /dev/null +++ b/pages/my-activities/my-activities.wxml @@ -0,0 +1,75 @@ + + + + + + + 我的活动 + + + + + + + + + 未开始 + + + + 已结束 + + + + + + + + + + + + {{item.time_status_text}} + + + + {{item.title}} + + + + + {{item.date_display}} + + + + {{item.location}} + + + + + + + {{item.status === 'confirmed' ? '已确认' : (item.status === 'pending' ? '待确认' : '已取消')}} + + + {{item.price_text || (item.is_free ? '免费' : '¥' + item.price)}} + + + + + + 正在加载... + 没有更多活动了 + + + + + + + 暂无活动记录 + + + + + + diff --git a/pages/my-activities/my-activities.wxss b/pages/my-activities/my-activities.wxss new file mode 100644 index 0000000..ec943f7 --- /dev/null +++ b/pages/my-activities/my-activities.wxss @@ -0,0 +1,224 @@ +.page-container { + min-height: 100vh; + background-color: #F9FAFB; + display: flex; + flex-direction: column; +} + +.main-content { + flex: 1; + display: flex; + flex-direction: column; + padding-top: 194rpx; /* Height of unified-header */ +} + +/* Tab 切换 */ +.tab-section { + background: #fff; + padding: 0 32rpx; + position: sticky; + top: 194rpx; + z-index: 10; +} + +.tab-list { + display: flex; + height: 88rpx; + border-bottom: 2rpx solid #F3F4F6; +} + +.tab-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: 32rpx; + color: #6B7280; + position: relative; + transition: all 0.2s ease; +} + +.tab-item.active { + color: #914584; + font-weight: bold; +} + +.active-line { + position: absolute; + bottom: 0; + width: 40rpx; + height: 4rpx; + background: #914584; + border-radius: 4rpx; +} + +/* 列表滚动区域 */ +.list-scroll { + flex: 1; + height: 0; /* Use flex:1 and height:0 for scroll-view in flex column */ +} + +.activity-list { + padding: 32rpx; +} + +/* 活动卡片 */ +.activity-card { + background: #fff; + border-radius: 32rpx; + margin-bottom: 32rpx; + overflow: hidden; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); + display: flex; + transition: transform 0.2s ease; +} + +.activity-card:active { + transform: scale(0.98); +} + +.card-image-wrap { + width: 200rpx; + height: 200rpx; + flex-shrink: 0; + position: relative; +} + +.card-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.time-status-badge { + position: absolute; + top: 0; + left: 0; + font-size: 20rpx; + padding: 4rpx 12rpx; + border-bottom-right-radius: 12rpx; + color: #fff; + z-index: 1; +} + +.time-status-badge.pending { background: #0EA5E9; } +.time-status-badge.started { background: #10B981; } +.time-status-badge.finished { background: #9CA3AF; } + +.card-info { + flex: 1; + padding: 20rpx 24rpx; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.activity-title { + font-size: 32rpx; + font-weight: bold; + color: #111827; + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + margin-bottom: 12rpx; +} + +.meta-info { + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.meta-item { + display: flex; + align-items: center; + gap: 8rpx; + font-size: 24rpx; + color: #6B7280; +} + +.card-footer { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 16rpx; +} + +.status-tags { + display: flex; + gap: 12rpx; +} + +.status-tag { + font-size: 20rpx; + padding: 4rpx 12rpx; + border-radius: 8rpx; +} + +.status-tag.pending { + background: #FEF3C7; + color: #D97706; +} + +.status-tag.confirmed { + background: #ECFDF5; + color: #059669; +} + +.status-tag.cancelled { + background: #FEE2E2; + color: #DC2626; +} + +.price-text { + font-size: 28rpx; + font-weight: bold; + color: #914584; +} + +/* 加载状态 */ +.load-status { + text-align: center; + padding: 32rpx; + font-size: 24rpx; + color: #9CA3AF; +} + +/* 空状态 */ +.empty-state { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-bottom: 200rpx; +} + +.empty-icon { + width: 240rpx; + height: 240rpx; + margin-bottom: 32rpx; + opacity: 0.6; +} + +.empty-text { + font-size: 28rpx; + color: #9CA3AF; + margin-bottom: 48rpx; +} + +.go-btn { + width: 240rpx; + height: 80rpx; + border-radius: 40rpx; + font-size: 28rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.safe-bottom-spacer { + height: env(safe-area-inset-bottom); +} diff --git a/pages/notices/detail/detail.js b/pages/notices/detail/detail.js new file mode 100644 index 0000000..dda1a54 --- /dev/null +++ b/pages/notices/detail/detail.js @@ -0,0 +1,64 @@ +const api = require('../../../utils/api') + +Page({ + data: { + notice: null, + loading: true, + error: null + }, + + onLoad(options) { + const id = options.id + if (id) { + this.loadNoticeDetail(id) + } + }, + + onBack() { + wx.navigateBack() + }, + + async loadNoticeDetail(id) { + this.setData({ loading: true, error: null }) + + try { + const res = await api.common.getNotices() + console.log('[notice-detail] 公告列表响应:', res) + + if (res.success && res.data) { + const noticeList = res.data.filter(item => String(item.id) === String(id)) + + if (noticeList.length > 0) { + const item = noticeList[0] + this.setData({ + notice: { + id: item.id, + content: item.content, + linkType: item.linkType || 'none', + linkValue: item.linkValue || '', + createdAt: this.formatDate(item.createdAt) + } + }) + } else { + this.setData({ error: '公告不存在' }) + } + } else { + this.setData({ error: res.error || '加载失败' }) + } + } catch (err) { + console.error('[notice-detail] 加载公告详情失败:', err) + this.setData({ error: err.message || '加载失败' }) + } finally { + this.setData({ loading: false }) + } + }, + + formatDate(dateStr) { + if (!dateStr) return '' + const date = new Date(dateStr) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` + } +}) diff --git a/pages/notices/detail/detail.json b/pages/notices/detail/detail.json new file mode 100644 index 0000000..8835af0 --- /dev/null +++ b/pages/notices/detail/detail.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/pages/notices/detail/detail.wxml b/pages/notices/detail/detail.wxml new file mode 100644 index 0000000..e9f5653 --- /dev/null +++ b/pages/notices/detail/detail.wxml @@ -0,0 +1,32 @@ + + + + + + 返回 + + 公告详情 + + + + + + + 加载中... + + + + + {{error}} + 返回 + + + + + + + + {{notice.createdAt}} + + + diff --git a/pages/notices/detail/detail.wxss b/pages/notices/detail/detail.wxss new file mode 100644 index 0000000..eb56539 --- /dev/null +++ b/pages/notices/detail/detail.wxss @@ -0,0 +1,118 @@ +.page { + min-height: 100vh; + background-color: #f8f8f8; + display: flex; + flex-direction: column; +} + +.unified-header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 194rpx; + background: linear-gradient(135deg, #B06AB3 0%, #9B59B6 100%); + border-bottom: none; + display: flex; + align-items: flex-end; + justify-content: space-between; + padding: 0 32rpx 20rpx; + z-index: 1000; +} + +.unified-header-left { + display: flex; + align-items: center; + gap: 8rpx; + width: 160rpx; + height: 56rpx; +} + +.unified-back-icon { + width: 56rpx; + height: 56rpx; +} + +.unified-back-text { + font-size: 34rpx; + font-weight: bold; + color: #ffffff; +} + +.unified-header-title { + font-size: 40rpx; + font-weight: bold; + color: #ffffff; + flex: 1; + text-align: center; +} + +.unified-header-right { + width: 160rpx; + height: 56rpx; +} + +.content { + flex: 1; +} + +.loading-tip, .error-tip { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: 200rpx; +} + +.loading-tip text, .error-tip text { + font-size: 30rpx; + color: #999; +} + +.error-tip text:first-child { + color: #ff6b6b; + margin-bottom: 20rpx; +} + +.retry-btn { + background: linear-gradient(135deg, #B06AB3 0%, #9B59B6 100%); + color: #fff !important; + padding: 16rpx 48rpx; + border-radius: 36rpx; + font-size: 32rpx !important; +} + +.notice-content { + padding: 30rpx; +} + +.notice-body { + background: #fff; + border-radius: 20rpx; + padding: 30rpx; +} + +.notice-body rich-text { + font-size: 32rpx; + color: #666; + line-height: 1.8; +} + +.notice-body p { + margin-bottom: 16rpx; +} + +.notice-body h3 { + font-size: 32rpx; + font-weight: bold; + color: #333; + margin: 24rpx 0 16rpx; +} + +.notice-date { + font-size: 32rpx; + color: #999; + text-align: right; + margin-top: 20rpx; + padding: 0 10rpx; +} diff --git a/pages/notices/notices.js b/pages/notices/notices.js new file mode 100644 index 0000000..f4e4f96 --- /dev/null +++ b/pages/notices/notices.js @@ -0,0 +1,77 @@ +const api = require('../../utils/api') + +Page({ + data: { + notices: [], + loading: true, + error: null + }, + + onLoad() { + this.loadNotices() + }, + + onBack() { + wx.navigateBack() + }, + + async loadNotices() { + this.setData({ loading: true, error: null }) + + try { + const res = await api.common.getNotices() + console.log('[notices] 公告列表响应:', res) + + if (res.success && res.data) { + const notices = res.data.map(item => ({ + id: item.id, + content: item.content, + linkType: item.linkType || 'none', + linkValue: item.linkValue || '', + sortOrder: item.sortOrder || 0, + createdAt: this.formatDate(item.createdAt) + })) + this.setData({ notices }) + } else { + this.setData({ error: res.error || '加载失败' }) + } + } catch (err) { + console.error('[notices] 加载公告失败:', err) + this.setData({ error: err.message || '加载失败' }) + } finally { + this.setData({ loading: false }) + } + }, + + onNoticeTap(e) { + const notice = e.currentTarget.dataset.notice + console.log('[notices] 点击公告:', notice) + + if (notice.linkType === 'web' && notice.linkValue) { + wx.navigateTo({ + url: `/pages/webview/webview?url=${encodeURIComponent(notice.linkValue)}` + }) + } else if (notice.linkType === 'miniprogram' && notice.linkValue) { + wx.navigateToMiniProgram({ + appId: notice.linkValue + }) + } else if (notice.linkType === 'article' && notice.linkValue) { + wx.navigateTo({ + url: `/pages/academy/detail/detail?id=${notice.linkValue}` + }) + } else { + wx.navigateTo({ + url: `/pages/notices/detail/detail?id=${notice.id}` + }) + } + }, + + formatDate(dateStr) { + if (!dateStr) return '' + const date = new Date(dateStr) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` + } +}) diff --git a/pages/notices/notices.json b/pages/notices/notices.json new file mode 100644 index 0000000..8835af0 --- /dev/null +++ b/pages/notices/notices.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/pages/notices/notices.wxml b/pages/notices/notices.wxml new file mode 100644 index 0000000..5beec10 --- /dev/null +++ b/pages/notices/notices.wxml @@ -0,0 +1,43 @@ + + + + + + 返回 + + 公告 + + + + + + + 加载中... + + + + + {{error}} + 点击重试 + + + + + + + + + + 点击查看详情 > + + {{item.createdAt}} + + + + + + + 暂无公告 + + + diff --git a/pages/notices/notices.wxss b/pages/notices/notices.wxss new file mode 100644 index 0000000..041d13d --- /dev/null +++ b/pages/notices/notices.wxss @@ -0,0 +1,140 @@ +.page { + min-height: 100vh; + background-color: #f8f8f8; + display: flex; + flex-direction: column; +} + +.unified-header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 194rpx; + background: linear-gradient(135deg, #B06AB3 0%, #9B59B6 100%); + border-bottom: none; + display: flex; + align-items: flex-end; + justify-content: space-between; + padding: 0 32rpx 20rpx; + z-index: 1000; +} + +.unified-header-left { + display: flex; + align-items: center; + gap: 8rpx; + width: 160rpx; + height: 56rpx; +} + +.unified-back-icon { + width: 56rpx; + height: 56rpx; +} + +.unified-back-text { + font-size: 34rpx; + font-weight: bold; + color: #ffffff; +} + +.unified-header-title { + font-size: 40rpx; + font-weight: bold; + color: #ffffff; + flex: 1; + text-align: center; +} + +.unified-header-right { + width: 160rpx; + height: 56rpx; +} + +.content { + flex: 1; +} + +.loading-tip, .error-tip, .empty-tip { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: 200rpx; +} + +.loading-tip text, .empty-tip text, .error-tip text { + font-size: 28rpx; + color: #999; +} + +.error-tip text:first-child { + color: #ff6b6b; + margin-bottom: 20rpx; +} + +.retry-btn { + background: linear-gradient(135deg, #B06AB3 0%, #9B59B6 100%); + color: #fff !important; + padding: 16rpx 48rpx; + border-radius: 36rpx; + font-size: 28rpx !important; +} + +.empty-tip image { + width: 240rpx; + height: 240rpx; + margin-bottom: 20rpx; +} + +.notices-list { + padding: 30rpx; +} + +.notice-item { + background: #fff; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 20rpx; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); +} + +.notice-content { + font-size: 32rpx; + color: #333; + line-height: 1.8; +} + +.notice-content rich-text { + line-height: 1.8; +} + +.notice-content p { + margin-bottom: 16rpx; +} + +.notice-content h3 { + font-size: 36rpx; + font-weight: bold; + color: #333; + margin: 24rpx 0 16rpx; +} + +.notice-footer { + margin-top: 20rpx; + padding-top: 20rpx; + border-top: 2rpx solid #f0f0f0; +} + +.link-hint { + font-size: 30rpx; + color: #B06AB3; +} + +.notice-date { + font-size: 30rpx; + color: #999; + margin-top: 16rpx; + text-align: right; +} diff --git a/pages/order-detail/order-detail.js b/pages/order-detail/order-detail.js new file mode 100644 index 0000000..1b1f8ae --- /dev/null +++ b/pages/order-detail/order-detail.js @@ -0,0 +1,331 @@ +// pages/order-detail/order-detail.js +// 订单详情页面 - 对接后端API + +const api = require('../../utils/api') + +// 评价标签预设 +const REVIEW_TAGS = [ + '专业', '耐心', '温暖', '有效果', '善于倾听', + '有同理心', '高效', '支持', '不评判', '细心', + '温柔', '有帮助', '值得信赖', '回复及时' +] + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + loading: true, + order: null, + // 评价弹窗相关 + showReviewModal: false, + reviewRating: 5, + reviewContent: '', + reviewTags: REVIEW_TAGS, + selectedTags: [], + isAnonymous: false, + submittingReview: false + }, + + onLoad(options) { + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + const orderId = options.id + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight + }) + + if (orderId) { + this.loadOrderDetail(orderId) + } else { + wx.showToast({ title: '参数错误', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + } + }, + + /** + * 加载订单详情 + */ + async loadOrderDetail(id) { + this.setData({ loading: true }) + + try { + const res = await api.order.getDetail(id) + + if (res.success && res.data) { + const order = this.transformOrder(res.data) + this.setData({ order, loading: false }) + } else { + throw new Error(res.message || '加载失败') + } + } catch (err) { + console.error('加载订单详情失败', err) + this.setData({ loading: false }) + wx.showToast({ title: '加载失败', icon: 'none' }) + } + }, + + /** + * 转换订单数据格式 + */ + transformOrder(data) { + const typeConfig = { + recharge: { typeName: '爱心充值', icon: '/images/icon-heart.png', iconBg: 'pink' }, + vip: { typeName: '会员服务', icon: '/images/icon-star.png', iconBg: 'purple' }, + gift: { typeName: '礼物购买', icon: '/images/icon-gift.png', iconBg: 'orange' }, + companion: { typeName: '陪聊服务', icon: '/images/icon-chat.png', iconBg: 'blue' } + } + + const config = typeConfig[data.type] || typeConfig.recharge + + const statusMap = { + pending: '待支付', + paid: '交易成功', + completed: '交易成功', + cancelled: '已取消', + refunded: '已退款', + in_service: '服务中' + } + + const statusDescMap = { + pending: '请尽快完成支付', + paid: '您的订单已完成支付', + completed: '订单已完成', + cancelled: '订单已取消', + refunded: '订单已退款', + in_service: '服务进行中' + } + + return { + id: data.id, + typeName: config.typeName, + icon: config.icon, + iconBg: config.iconBg, + status: statusMap[data.status] || data.status, + statusDesc: statusDescMap[data.status] || '', + productName: data.product_name || data.description || '', + priceSymbol: data.currency === 'flower' ? '🌿' : '¥', + price: data.amount || data.price || '0', + payMethod: data.payment_method === 'wechat' ? '微信支付' : (data.payment_method === 'flower' ? '爱心支付' : data.payment_method || ''), + orderNo: data.order_no || data.id, + createTime: this.formatTime(data.created_at), + payTime: data.paid_at ? this.formatTime(data.paid_at) : '', + canRefund: data.status === 'paid' && data.can_refund !== false, + canCancel: data.status === 'pending', + canReview: data.status === 'completed' && !data.reviewed + } + }, + + /** + * 格式化时间 + */ + formatTime(dateStr) { + if (!dateStr) return '' + const date = new Date(dateStr) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hour = String(date.getHours()).padStart(2, '0') + const minute = String(date.getMinutes()).padStart(2, '0') + const second = String(date.getSeconds()).padStart(2, '0') + return `${year}-${month}-${day} ${hour}:${minute}:${second}` + }, + + goBack() { + wx.navigateBack() + }, + + onCopyOrderNo() { + const { orderNo } = this.data.order + wx.setClipboardData({ + data: String(orderNo), + success: () => { + wx.showToast({ + title: '已复制订单号', + icon: 'success' + }) + } + }) + }, + + onContactService() { + wx.navigateTo({ + url: '/pages/service/service' + }) + }, + + /** + * 取消订单 + */ + async onCancel() { + const confirmed = await this.showConfirm('取消订单', '确定要取消订单吗?') + if (!confirmed) return + + wx.showLoading({ title: '取消中...' }) + + try { + const res = await api.order.cancel(this.data.order.id) + + wx.hideLoading() + + if (res.success) { + wx.showToast({ title: '订单已取消', icon: 'success' }) + // 刷新订单详情 + this.loadOrderDetail(this.data.order.id) + } else { + wx.showToast({ title: res.message || '取消失败', icon: 'none' }) + } + } catch (err) { + wx.hideLoading() + wx.showToast({ title: '取消失败', icon: 'none' }) + } + }, + + /** + * 申请退款 + */ + async onRefund() { + const confirmed = await this.showConfirm('申请退款', '确定要申请退款吗?') + if (!confirmed) return + + wx.showLoading({ title: '提交中...' }) + + try { + const res = await api.order.cancel(this.data.order.id, '用户申请退款') + + wx.hideLoading() + + if (res.success) { + wx.showToast({ title: '退款申请已提交', icon: 'success' }) + this.loadOrderDetail(this.data.order.id) + } else { + wx.showToast({ title: res.message || '申请失败', icon: 'none' }) + } + } catch (err) { + wx.hideLoading() + wx.showToast({ title: '申请失败', icon: 'none' }) + } + }, + + /** + * 评价订单 - 打开评价弹窗 + */ + onReview() { + this.setData({ + showReviewModal: true, + reviewRating: 5, + reviewContent: '', + selectedTags: [], + isAnonymous: false + }) + }, + + /** + * 关闭评价弹窗 + */ + closeReviewModal() { + this.setData({ showReviewModal: false }) + }, + + /** + * 选择评分 + */ + selectRating(e) { + const rating = e.currentTarget.dataset.rating + this.setData({ reviewRating: rating }) + }, + + /** + * 切换标签选择 + */ + toggleTag(e) { + const tag = e.currentTarget.dataset.tag + const { selectedTags } = this.data + const index = selectedTags.indexOf(tag) + + if (index > -1) { + selectedTags.splice(index, 1) + } else { + if (selectedTags.length < 5) { + selectedTags.push(tag) + } else { + wx.showToast({ title: '最多选择5个标签', icon: 'none' }) + return + } + } + + this.setData({ selectedTags }) + }, + + /** + * 输入评价内容 + */ + onReviewInput(e) { + this.setData({ reviewContent: e.detail.value }) + }, + + /** + * 切换匿名评价 + */ + toggleAnonymous() { + this.setData({ isAnonymous: !this.data.isAnonymous }) + }, + + /** + * 提交评价 + */ + async submitReview() { + const { order, reviewRating, reviewContent, selectedTags, isAnonymous } = this.data + + if (this.data.submittingReview) return + + this.setData({ submittingReview: true }) + wx.showLoading({ title: '提交中...' }) + + try { + const res = await api.order.review(order.id, { + rating: reviewRating, + content: reviewContent, + tags: selectedTags, + isAnonymous: isAnonymous + }) + + wx.hideLoading() + this.setData({ submittingReview: false }) + + if (res.success) { + wx.showToast({ title: '评价成功', icon: 'success' }) + this.setData({ showReviewModal: false }) + // 刷新订单详情 + this.loadOrderDetail(order.id) + } else { + wx.showToast({ title: res.message || res.error || '评价失败', icon: 'none' }) + } + } catch (err) { + wx.hideLoading() + this.setData({ submittingReview: false }) + console.error('提交评价失败', err) + wx.showToast({ title: '评价失败', icon: 'none' }) + } + }, + + /** + * 显示确认弹窗 + */ + showConfirm(title, content) { + return new Promise((resolve) => { + wx.showModal({ + title, + content, + success: (res) => resolve(res.confirm) + }) + }) + } +}) diff --git a/pages/order-detail/order-detail.json b/pages/order-detail/order-detail.json new file mode 100644 index 0000000..e90e996 --- /dev/null +++ b/pages/order-detail/order-detail.json @@ -0,0 +1,4 @@ +{ + "usingComponents": {}, + "navigationStyle": "custom" +} diff --git a/pages/order-detail/order-detail.wxml b/pages/order-detail/order-detail.wxml new file mode 100644 index 0000000..9ceadb7 --- /dev/null +++ b/pages/order-detail/order-detail.wxml @@ -0,0 +1,152 @@ + + + + + + + + + + 订单详情 + + + + + + + + + + + + {{order.status}} + {{order.statusDesc}} + + + + + 订单信息 + + + 订单类型 + {{order.typeName}} + + + + 商品名称 + {{order.productName}} + + + + 订单金额 + + {{order.priceSymbol}} + {{order.price}} + + + + + 支付方式 + {{order.payMethod}} + + + + + + 订单编号 + + + 订单号 + + {{order.orderNo}} + 复制 + + + + + 创建时间 + {{order.createTime}} + + + + 支付时间 + {{order.payTime}} + + + + + + 联系客服 + 评价订单 + 申请退款 + + + + + + + + + + 评价订单 + + × + + + + + + + + 服务评分 + + + + + + + + {{reviewRating}}分 - {{reviewRating >= 4 ? '非常满意' : (reviewRating >= 3 ? '一般' : '不满意')}} + + + + + 选择标签(最多5个) + + + {{item}} + + + + + + + 评价内容 + + {{reviewContent.length}}/500 + + + + + + + + 匿名评价 + + + + + + + + diff --git a/pages/order-detail/order-detail.wxss b/pages/order-detail/order-detail.wxss new file mode 100644 index 0000000..57e7ca7 --- /dev/null +++ b/pages/order-detail/order-detail.wxss @@ -0,0 +1,421 @@ +/* 订单详情页样式 */ +.page-container { + min-height: 100vh; + background: #faf8fc; +} + +/* 自定义导航栏 */ +.custom-nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + background: #faf8fc; +} + +.status-bar { + width: 100%; +} + +.nav-bar { + display: flex; + align-items: center; + justify-content: space-between; + height: 88rpx; + padding: 0 32rpx; +} + +.nav-left { + width: 80rpx; + display: flex; + align-items: center; +} + +.nav-back-icon { + width: 48rpx; + height: 48rpx; +} + +.nav-title { + flex: 1; + text-align: center; + font-size: 34rpx; + font-weight: 600; + color: #101828; +} + +.nav-right { + width: 80rpx; +} + +/* 内容区域 */ +.content { + height: 100vh; + padding: 32rpx; + box-sizing: border-box; +} + +/* 订单状态卡片 */ +.status-card { + background: #fff; + border-radius: 32rpx; + padding: 48rpx 32rpx; + margin-bottom: 24rpx; + display: flex; + flex-direction: column; + align-items: center; + box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.08); +} + +.status-icon-wrap { + width: 120rpx; + height: 120rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 24rpx; +} + +.status-icon-wrap.pink { + background: #fdf2f8; +} + +.status-icon-wrap.purple { + background: #faf5ff; +} + +.status-icon-wrap.orange { + background: #fff7ed; +} + +.status-icon-wrap.green { + background: #ecfdf5; +} + +.status-icon { + width: 56rpx; + height: 56rpx; +} + +.status-title { + font-size: 40rpx; + font-weight: 700; + color: #101828; + margin-bottom: 12rpx; +} + +.status-desc { + font-size: 28rpx; + color: #6a7282; +} + +/* 信息卡片 */ +.info-card { + background: #fff; + border-radius: 32rpx; + padding: 32rpx; + margin-bottom: 24rpx; + box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.08); +} + +.card-title { + font-size: 32rpx; + font-weight: 700; + color: #101828; + margin-bottom: 24rpx; + padding-bottom: 20rpx; + border-bottom: 2rpx solid #f3f4f6; +} + +.info-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20rpx 0; +} + +.info-label { + font-size: 28rpx; + color: #6a7282; +} + +.info-value { + font-size: 28rpx; + color: #101828; + font-weight: 500; +} + +.info-price { + display: flex; + align-items: baseline; + gap: 4rpx; +} + +.info-price .price-symbol { + font-size: 28rpx; + font-weight: 700; + color: #ff4d6d; +} + +.info-price .price-value { + font-size: 36rpx; + font-weight: 900; + color: #ff4d6d; +} + +.info-copy { + display: flex; + align-items: center; + gap: 16rpx; +} + +.copy-btn { + font-size: 24rpx; + color: #ff4d6d; + padding: 8rpx 16rpx; + background: #fff0f3; + border-radius: 12rpx; +} + +/* 底部按钮 */ +.bottom-actions { + display: flex; + gap: 24rpx; + padding: 32rpx 0 120rpx; +} + +.action-btn { + flex: 1; + height: 88rpx; + border-radius: 44rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 30rpx; + font-weight: 600; +} + +.action-btn.outline { + background: #fff; + border: 2rpx solid #e5e7eb; + color: #364153; +} + +.action-btn.primary { + background: #ff4d6d; + color: #fff; +} + + +/* ========== 评价弹窗样式 ========== */ +.review-modal-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; +} + +.review-modal { + position: fixed; + left: 0; + right: 0; + bottom: 0; + background: #fff; + border-radius: 32rpx 32rpx 0 0; + z-index: 1001; + max-height: 85vh; + display: flex; + flex-direction: column; + animation: slideUp 0.3s ease; +} + +@keyframes slideUp { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} + +.review-modal.show { + transform: translateY(0); +} + +.review-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 32rpx; + border-bottom: 2rpx solid #f3f4f6; +} + +.review-modal-title { + font-size: 36rpx; + font-weight: 700; + color: #101828; +} + +.review-modal-close { + width: 48rpx; + height: 48rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.close-icon { + font-size: 48rpx; + color: #6a7282; + line-height: 1; +} + +.review-modal-content { + flex: 1; + padding: 32rpx; + overflow-y: auto; + max-height: 60vh; +} + +/* 评价区块 */ +.review-section { + margin-bottom: 32rpx; +} + +.section-title { + font-size: 28rpx; + font-weight: 600; + color: #101828; + margin-bottom: 16rpx; + display: block; +} + +/* 评分星星 */ +.rating-stars { + display: flex; + gap: 16rpx; + margin-bottom: 12rpx; +} + +.star-item { + font-size: 48rpx; + opacity: 0.3; + transition: all 0.2s; +} + +.star-item.active { + opacity: 1; +} + +.rating-text { + font-size: 24rpx; + color: #6a7282; +} + +/* 标签列表 */ +.tag-list { + display: flex; + flex-wrap: wrap; + gap: 16rpx; +} + +.tag-item { + padding: 12rpx 24rpx; + background: #f3f4f6; + border-radius: 100rpx; + font-size: 26rpx; + color: #4a5565; + border: 2rpx solid transparent; + transition: all 0.2s; +} + +.tag-item.active { + background: #fff0f3; + color: #ff4d6d; + border-color: #ff4d6d; +} + +/* 评价输入框 */ +.review-textarea { + width: 100%; + height: 200rpx; + padding: 24rpx; + background: #f9fafb; + border-radius: 16rpx; + font-size: 28rpx; + color: #101828; + box-sizing: border-box; +} + +.char-count { + display: block; + text-align: right; + font-size: 24rpx; + color: #99a1af; + margin-top: 8rpx; +} + +/* 匿名选项 */ +.anonymous-section { + display: flex; + align-items: center; + gap: 16rpx; + padding: 16rpx 0; +} + +.checkbox { + width: 40rpx; + height: 40rpx; + border: 2rpx solid #d1d5db; + border-radius: 8rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 24rpx; + color: #fff; + transition: all 0.2s; +} + +.checkbox.checked { + background: #ff4d6d; + border-color: #ff4d6d; +} + +.anonymous-text { + font-size: 28rpx; + color: #4a5565; +} + +/* 提交按钮 */ +.review-modal-footer { + padding: 24rpx 32rpx; + padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); + border-top: 2rpx solid #f3f4f6; +} + +.submit-btn { + width: 100%; + height: 96rpx; + background: linear-gradient(to right, #ff4d6d, #e91e63); + color: #fff; + font-size: 32rpx; + font-weight: 600; + border-radius: 48rpx; + border: none; + display: flex; + align-items: center; + justify-content: center; +} + +.submit-btn.disabled { + opacity: 0.6; +} + +.submit-btn::after { + border: none; +} diff --git a/pages/orders/orders.js b/pages/orders/orders.js new file mode 100644 index 0000000..00dda03 --- /dev/null +++ b/pages/orders/orders.js @@ -0,0 +1,118 @@ +const api = require('../../utils/api'); + +Page({ + data: { + statusBarHeight: 20, + navBarHeight: 44, + totalNavHeight: 64, + loading: true, + currentTab: 'all', // all, pending, paid, completed + list: [] + }, + onLoad() { + const sys = wx.getSystemInfoSync(); + const menu = wx.getMenuButtonBoundingClientRect(); + const statusBarHeight = sys.statusBarHeight || 20; + const navBarHeight = menu.height + (menu.top - statusBarHeight) * 2; + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight: statusBarHeight + navBarHeight + }); + this.load(); + }, + onBack() { + wx.navigateBack({ delta: 1 }); + }, + async load(isRefresh = false) { + if (!isRefresh) { + this.setData({ loading: true }); + } + try { + // Use payment.getOrders to show the recharge/vip/etc orders + // Pass confirm: 1 on initial load or refresh to ensure status is up to date + const params = { page: 1, pageSize: 50, confirm: 1 }; + if (this.data.currentTab !== 'all') { + params.status = this.data.currentTab; + } + const res = await api.payment.getOrders(params); + // Get list from response structure + let orders = []; + if (res.data && Array.isArray(res.data.list)) { + orders = res.data.list; + } else if (res.data && Array.isArray(res.data.orders)) { + orders = res.data.orders; + } else if (Array.isArray(res.data)) { + orders = res.data; + } + + const list = orders.map((o) => ({ + id: o.id || o.orderNo, + remark: this.formatOrderType(o.orderType || 'order'), + amountText: this.formatAmount(o.amount), + status: this.formatStatus(o.status), + statusClass: `status-${o.status}`, + createdAtText: this.formatDateTime(new Date(o.createdAt || o.created_at || Date.now())), + // visual adjustment: ensure it looks like income/recharge style + transactionType: o.orderType || 'recharge' + })); + + this.setData({ list }); + } catch (err) { + console.error('API failed', err); + // Fallback empty list + if (!isRefresh) { + this.setData({ list: [] }); + } + } finally { + this.setData({ loading: false }); + if (isRefresh) { + wx.stopPullDownRefresh(); + } + } + }, + onPullDownRefresh() { + this.load(true); + }, + switchTab(e) { + const tab = e.currentTarget.dataset.tab; + if (tab === this.data.currentTab) return; + this.setData({ currentTab: tab }, () => { + this.load(); + }); + }, + formatOrderType(type) { + const map = { + 'recharge': '充值', + 'vip': '会员购买', + 'agent_purchase': '智能体购买', + 'companion_chat': '陪聊服务', + 'identity_card': '身份卡' + }; + return map[String(type)] || '订单'; + }, + formatStatus(status) { + const map = { + 'pending': '待支付', + 'paid': '已支付', + 'refunding': '退款中', + 'refunded': '已退款', + 'cancelled': '已取消', + 'completed': '已完成' + }; + return map[String(status)] || status; + }, + formatDateTime(d) { + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const hh = String(d.getHours()).padStart(2, '0'); + const mm = String(d.getMinutes()).padStart(2, '0'); + return `${y}-${m}-${day} ${hh}:${mm}`; + }, + formatAmount(amount) { + const n = Number(amount || 0); + return `¥${n.toFixed(2)}`; + } +}); + diff --git a/pages/orders/orders.json b/pages/orders/orders.json new file mode 100644 index 0000000..8fddf0b --- /dev/null +++ b/pages/orders/orders.json @@ -0,0 +1,7 @@ +{ + "usingComponents": { + "app-icon": "/components/icon/icon" + }, + "enablePullDownRefresh": true, + "backgroundTextStyle": "dark" +} diff --git a/pages/orders/orders.wxml b/pages/orders/orders.wxml new file mode 100644 index 0000000..389b8d3 --- /dev/null +++ b/pages/orders/orders.wxml @@ -0,0 +1,40 @@ + + + + + + 返回 + + 我的订单 + + + + + 全部 + 待支付 + 已支付 + 已完成 + + + + + + 加载中... + 暂无数据 + + + + {{item.remark || item.transactionType}} + {{item.createdAtText}} + + + {{item.amountText}} + {{item.status}} + + + + + + + + diff --git a/pages/orders/orders.wxss b/pages/orders/orders.wxss new file mode 100644 index 0000000..6c279fc --- /dev/null +++ b/pages/orders/orders.wxss @@ -0,0 +1,136 @@ +.page { + min-height: 100vh; + background: #E8C3D4; +} + +/* 顶部导航栏已移除,改用全局 unified-header */ + +.wrap { + padding: 0 32rpx; +} + +.tabs-fixed { + position: fixed; + left: 0; + right: 0; + height: 120rpx; + background: #ffffff; + display: flex; + align-items: center; + justify-content: space-around; + z-index: 100; + border-bottom: 2rpx solid #f3f4f6; +} + +.tab-item { + font-size: 28rpx; + color: #6b7280; + font-weight: 600; + position: relative; + height: 80rpx; + line-height: 100rpx; +} + +.tab-item.active { + color: #b06ab3; + font-weight: 900; +} + +.tab-item.active::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 40rpx; + height: 6rpx; + background: #b06ab3; + border-radius: 3rpx; +} + +.card { + background: #ffffff; + border-radius: 40rpx; + padding: 24rpx; + box-shadow: 0 10rpx 20rpx rgba(17, 24, 39, 0.04); +} + +.loading, +.empty { + text-align: center; + color: #9ca3af; + font-weight: 800; + padding: 80rpx 0; +} + +.row { + padding: 24rpx 8rpx; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 2rpx solid #f3f4f6; +} + +.row:last-child { + border-bottom: 0; +} + +.row-left { + flex: 1; + min-width: 0; +} + +.row-title { + display: block; + font-size: 34rpx; + font-weight: 900; + color: #111827; +} + +.row-sub { + display: block; + margin-top: 10rpx; + font-size: 26rpx; + color: #9ca3af; + font-weight: 600; +} + +.row-right { + text-align: right; +} + +.row-amount { + display: block; + font-size: 36rpx; + font-weight: 900; + color: #b06ab3; +} + +.row-status { + display: block; + margin-top: 10rpx; + font-size: 26rpx; + color: #6b7280; + font-weight: 700; +} + +.status-completed { + color: #10B981; +} + +.status-paid { + color: #3B82F6; +} + +.status-pending { + color: #F59E0B; +} + +.status-cancelled { + color: #9CA3AF; +} + +.status-refunded { + color: #EF4444; +} + diff --git a/pages/outdoor-activities/outdoor-activities.js b/pages/outdoor-activities/outdoor-activities.js new file mode 100644 index 0000000..80ec686 --- /dev/null +++ b/pages/outdoor-activities/outdoor-activities.js @@ -0,0 +1,374 @@ +// pages/outdoor-activities/outdoor-activities.js - 户外郊游页面 +const api = require('../../utils/api') +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + loading: false, + loadingMore: false, + activeTab: 'featured', + + // 活动列表 + activityList: [], + + // 分页相关 + page: 1, + limit: 20, + hasMore: true, + total: 0, + + // 二维码弹窗 + showQrcodeModal: false, + qrcodeImageUrl: '' + }, + + onLoad() { + // 计算导航栏高度 + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight + }) + + this.loadActivityList() + }, + + /** + * 返回上一页 + */ + onBack() { + wx.navigateBack() + }, + + /** + * 切换活动标签 + */ + onTabChange(e) { + const tab = e.currentTarget.dataset.tab + if (tab === this.data.activeTab) return + + this.setData({ + activeTab: tab, + activityList: [], + page: 1, + hasMore: true + }) + this.loadActivityList() + }, + + /** + * 下拉刷新 + */ + onPullDownRefresh() { + this.loadActivityList(false).finally(() => { + wx.stopPullDownRefresh() + }) + }, + + /** + * 上拉加载更多 + */ + onReachBottom() { + if (this.data.hasMore && !this.data.loadingMore && !this.data.loading) { + this.loadActivityList(true) + } + }, + + /** + * 加载活动列表 - 根据categoryName筛选户外郊游(支持分页) + */ + async loadActivityList(isLoadMore = false) { + if (isLoadMore) { + this.setData({ loadingMore: true }) + } else { + this.setData({ loading: true, page: 1, hasMore: true, activityList: [] }) + } + + try { + const { activeTab, page, limit } = this.data + const params = { + category: 'outdoor', + limit: limit, + page: page + } + + if (activeTab === 'featured') { + params.tab = 'featured' + } else if (activeTab === 'free') { + params.priceType = 'free' + } else if (activeTab === 'vip') { + params.is_vip = true + } else if (activeTab === 'svip') { + params.is_svip = true + } + + const res = await api.activity.getList(params) + + if (res.success && res.data && res.data.list) { + const total = res.data.total || 0 + const allActivities = res.data.list + const outdoorActivities = allActivities.filter(item => item.categoryName === '户外郊游') + + let clubQrcode = '' + const firstWithQrcode = outdoorActivities.find(item => item.activityGuideQrcode || item.activity_guide_qrcode) + if (firstWithQrcode && !isLoadMore) { + clubQrcode = firstWithQrcode.activityGuideQrcode || firstWithQrcode.activity_guide_qrcode + } + + const newActivityList = outdoorActivities.map(item => { + const heat = item.heat || (item.likes * 2 + (item.views || 0) + (item.current_participants || 0) * 3) + + return { + id: item.id, + title: item.title, + date: this.formatDate(item.start_date || item.activityDate), + location: item.location || '', + venue: item.venue || '', + image: item.coverImage || item.cover_image || '', + heat: Math.floor(heat), + price: item.price_text || item.priceText || '免费', + priceType: item.is_free || item.priceType === 'free' ? 'free' : 'paid', + likes: item.likes || item.likesCount || 0, + participants: item.current_participants || item.currentParticipants || 0, + isLiked: item.is_liked || item.isLiked || false, + isSignedUp: item.is_registered || item.isSignedUp || false, + status: item.status || (item.currentParticipants >= item.maxParticipants && item.maxParticipants > 0 ? 'full' : 'upcoming'), + activityGuideQrcode: item.activityGuideQrcode || item.activity_guide_qrcode || '' + } + }) + + const hasMore = newActivityList.length >= limit && (this.data.activityList.length + newActivityList.length) < total + + if (isLoadMore) { + this.setData({ + activityList: [...this.data.activityList, ...newActivityList], + loadingMore: false, + hasMore, + page: this.data.page + 1, + total + }) + } else { + this.setData({ + activityList: newActivityList, + hasMore, + total, + qrcodeImageUrl: clubQrcode || this.data.qrcodeImageUrl || 'https://ai-c.maimanji.com/images/outdoor-group-qrcode.png' + }) + } + + console.log('[outdoor-activities] 加载成功,总数:', total, '当前:', this.data.activityList.length, 'hasMore:', hasMore) + } else { + if (isLoadMore) { + this.setData({ loadingMore: false, hasMore: false }) + } else { + this.setData({ activityList: [], hasMore: false }) + } + } + } catch (err) { + console.error('加载活动列表失败', err) + if (isLoadMore) { + this.setData({ loadingMore: false }) + } else { + this.setData({ activityList: [], loading: false }) + } + } finally { + if (!isLoadMore) { + this.setData({ loading: false }) + } + } + }, + + /** + * 格式化日期 + */ + formatDate(dateStr) { + if (!dateStr) return '' + const date = new Date(dateStr) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}年${month}月${day}日` + }, + + /** + * 点击活动卡片 + */ + onActivityTap(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/activity-detail/activity-detail?id=${id}` + }) + }, + + /** + * 点赞 + */ + async onLike(e) { + const id = e.currentTarget.dataset.id + const index = e.currentTarget.dataset.index + + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ url: '/pages/login/login' }) + return + } + + try { + const res = await api.activity.toggleLike(id) + if (res.success) { + this.setData({ + [`activityList[${index}].isLiked`]: res.data.isLiked, + [`activityList[${index}].likes`]: res.data.likesCount + }) + } + } catch (err) { + console.error('点赞失败', err) + wx.showToast({ title: '操作失败', icon: 'none' }) + } + }, + + /** + * 立即报名 + */ + async onSignUp(e) { + const id = e.currentTarget.dataset.id + const index = e.currentTarget.dataset.index + + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ url: '/pages/login/login' }) + return + } + + const activity = this.data.activityList[index] + + // 检查活动状态 + if (activity.status === 'full' || activity.status === 'ended') { + const qrCode = activity.activityGuideQrcode || activity.activity_guide_qrcode || this.data.qrcodeImageUrl || 'https://ai-c.maimanji.com/api/common/qrcode?type=group' + this.setData({ + qrcodeImageUrl: qrCode, + showQrcodeModal: true + }) + return + } + + try { + if (activity.isSignedUp) { + // 取消报名 + const res = await api.activity.cancelSignup(id) + if (res.success) { + wx.showToast({ title: '已取消报名', icon: 'success' }) + this.loadActivityList() + } + } else { + // 报名 + const res = await api.activity.signup(id) + if (res.success) { + wx.showToast({ title: '报名成功', icon: 'success' }) + this.loadActivityList() + } else { + // 检查是否需要显示二维码(后端开关关闭或活动已结束) + if (res.code === 'QR_CODE_REQUIRED' || res.error === 'QR_CODE_REQUIRED' || res.code === 'ACTIVITY_ENDED' || res.error === '活动已结束') { + if (activity.activityGuideQrcode || activity.activity_guide_qrcode) { + this.setData({ qrcodeImageUrl: activity.activityGuideQrcode || activity.activity_guide_qrcode }) + } + this.setData({ showQrcodeModal: true }) + } else { + wx.showToast({ + title: res.error || '操作失败', + icon: 'none' + }) + } + } + } + } catch (err) { + console.error('报名操作失败', err) + // 捕获特定错误码以显示二维码 + const isQrRequired = err && (err.code === 'QR_CODE_REQUIRED' || (err.data && err.data.code === 'QR_CODE_REQUIRED')) + const isActivityEnded = err && (err.code === 'ACTIVITY_ENDED' || (err.data && err.data.code === 'ACTIVITY_ENDED') || err.error === '活动已结束') + + if (isQrRequired || isActivityEnded) { + if (activity.activityGuideQrcode || activity.activity_guide_qrcode) { + this.setData({ qrcodeImageUrl: activity.activityGuideQrcode || activity.activity_guide_qrcode }) + } + this.setData({ showQrcodeModal: true }) + + if (isActivityEnded) { + wx.showToast({ title: '活动已结束,进群查看更多', icon: 'none' }) + } + } else { + wx.showToast({ + title: err.error || err.message || '操作失败', + icon: 'none' + }) + } + } + }, + + /** + * 加入户外郊游群 + */ + onJoinGroup() { + // 如果没有二维码,尝试获取第一个活动的二维码 + if (!this.data.qrcodeImageUrl && this.data.activityList.length > 0) { + const firstWithQrcode = this.data.activityList.find(item => item.activityGuideQrcode || item.activity_guide_qrcode) + if (firstWithQrcode) { + this.setData({ qrcodeImageUrl: firstWithQrcode.activityGuideQrcode || firstWithQrcode.activity_guide_qrcode }) + } + } + this.setData({ showQrcodeModal: true }) + }, + + /** + * 关闭二维码弹窗 + */ + onCloseQrcodeModal() { + this.setData({ showQrcodeModal: false }) + }, + + /** + * 阻止冒泡 + */ + preventBubble() { + return + }, + + /** + * 保存二维码 + */ + async onSaveQrcode() { + try { + const { qrcodeImageUrl } = this.data + const downloadRes = await new Promise((resolve, reject) => { + wx.downloadFile({ + url: qrcodeImageUrl, + success: resolve, + fail: reject + }) + }) + + if (downloadRes.statusCode !== 200) throw new Error('下载失败') + + await new Promise((resolve, reject) => { + wx.saveImageToPhotosAlbum({ + filePath: downloadRes.tempFilePath, + success: resolve, + fail: reject + }) + }) + + wx.showToast({ title: '保存成功', icon: 'success' }) + this.onCloseQrcodeModal() + } catch (err) { + console.error('保存失败', err) + wx.showToast({ title: '保存失败', icon: 'none' }) + } + } +}) diff --git a/pages/outdoor-activities/outdoor-activities.json b/pages/outdoor-activities/outdoor-activities.json new file mode 100644 index 0000000..a3b4779 --- /dev/null +++ b/pages/outdoor-activities/outdoor-activities.json @@ -0,0 +1,9 @@ +{ + "navigationStyle": "custom", + "navigationBarTextStyle": "black", + "usingComponents": { + "app-icon": "../../components/icon/icon" + }, + "enablePullDownRefresh": true, + "backgroundTextStyle": "dark" +} diff --git a/pages/outdoor-activities/outdoor-activities.wxml b/pages/outdoor-activities/outdoor-activities.wxml new file mode 100644 index 0000000..d361608 --- /dev/null +++ b/pages/outdoor-activities/outdoor-activities.wxml @@ -0,0 +1,133 @@ + + + + + + + + + + 户外郊游 + + + + + + + + + 户外郊游俱乐部 + + 结伴同行 + 领略自然 + + + + 点击立即加入 + + + + + + + + + 精选活动 + + + 免费活动 + + + VIP活动 + + + SVIP活动 + + + + + + + + + + + + + + {{item.price}} + + + + {{item.title}} + + + + {{item.date}} + + + + + {{item.location}} + + + + {{item.heat}} + + + + + + + {{item.participants}}人已报名 + + + + + + + + + 暂无活动 + + + + 没有更多活动了 ~ + + + + + + + + + + + + + 加入户外郊游群 + 发现大自然之美,结交志同道合的朋友 + + + + 保存二维码 + + + diff --git a/pages/outdoor-activities/outdoor-activities.wxss b/pages/outdoor-activities/outdoor-activities.wxss new file mode 100644 index 0000000..bed3e97 --- /dev/null +++ b/pages/outdoor-activities/outdoor-activities.wxss @@ -0,0 +1,603 @@ +/* 户外郊游页面样式 - 清新绿色主题 */ +page { + background: linear-gradient(180deg, #C8E6C9 0%, #E8F5E9 100%); +} + +.page-container { + min-height: 100vh; + background: linear-gradient(180deg, #C8E6C9 0%, #E8F5E9 100%); +} + +/* 固定导航栏容器 */ +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; + background: rgba(248, 252, 249, 0.75); + backdrop-filter: blur(20rpx) saturate(180%); + -webkit-backdrop-filter: blur(20rpx) saturate(180%); +} + +/* 状态栏 */ +.status-bar { + background: transparent; +} + +/* 导航栏 */ +.nav-bar { + display: flex; + align-items: center; + justify-content: center; + padding: 0 32rpx; + background: transparent; + position: relative; +} + +.nav-back { + position: absolute; + left: 32rpx; + top: 50%; + transform: translateY(-50%); + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.back-icon { + width: 48rpx; + height: 48rpx; +} + +.nav-title { + font-size: 36rpx; + font-weight: 700; + color: #1A1A1A; + line-height: 1; +} + +/* 内容滚动区域 */ +.content-scroll { + height: 100vh; + box-sizing: border-box; +} + +/* 推广卡片 - 清新绿色渐变 */ +.city-group-card { + margin: 32rpx; + padding: 32rpx 40rpx; + min-height: 128rpx; + background: linear-gradient(135deg, + rgba(200, 230, 201, 0.6) 0%, + rgba(232, 245, 233, 0.6) 100%); + backdrop-filter: blur(16rpx) saturate(150%); + border: 2rpx solid rgba(76, 175, 80, 0.3); + border-radius: 48rpx; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 4rpx 20rpx rgba(76, 175, 80, 0.12); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.city-group-card:active { + transform: scale(0.98); + box-shadow: 0 2rpx 12rpx rgba(76, 175, 80, 0.18); +} + +.group-info { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8rpx; + padding-right: 24rpx; +} + +.group-title { + font-size: 40rpx; + font-weight: 700; + color: #1A1A1A; + line-height: 1.4; + white-space: nowrap; +} + +.group-tags { + display: flex; + flex-direction: column; + gap: 4rpx; +} + +.tag-item { + font-size: 28rpx; + font-weight: 500; + color: #4A5565; + line-height: 1.4; + white-space: nowrap; +} + +.join-btn { + padding: 0 40rpx; + height: 88rpx; + background: linear-gradient(135deg, #66BB6A 0%, #43A047 100%); + border-radius: 100rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + font-weight: 700; + color: #fff; + white-space: nowrap; + flex-shrink: 0; + box-shadow: 0 6rpx 24rpx rgba(76, 175, 80, 0.4), + 0 3rpx 12rpx rgba(76, 175, 80, 0.3); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; +} + +.join-btn:active { + transform: scale(0.96); + box-shadow: 0 4rpx 16rpx rgba(76, 175, 80, 0.45); +} + +/* 活动标签切换 - 横向滚动 */ +.tab-section { + padding: 32rpx 0; + background: transparent; + margin: 0 32rpx 32rpx; + position: relative; + z-index: 1; +} + +.tab-scroll { + width: 100%; + white-space: nowrap; +} + +.tab-scroll::-webkit-scrollbar { + display: none; +} + +.tab-list { + display: inline-flex; + gap: 20rpx; + padding: 0 4rpx; +} + +.tab-item { + padding: 20rpx 48rpx; + border-radius: 100rpx; + font-size: 32rpx; + font-weight: 700; + color: #6A7282; + background: rgba(255, 255, 255, 0.6); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + flex-shrink: 0; + white-space: nowrap; +} + +.tab-item:active { + transform: scale(0.96); +} + +.tab-item.active { + color: #fff; + background: linear-gradient(135deg, #66BB6A 0%, #43A047 100%); + box-shadow: 0 12rpx 24rpx rgba(76, 175, 80, 0.3); + transform: scale(1.02); +} + +/* 活动列表标题 */ +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 32rpx; + margin-bottom: 32rpx; +} + +.section-title { + font-size: 44rpx; + font-weight: 700; + color: #1A1A1A; +} + +.activity-count { + font-size: 28rpx; + color: #43A047; + font-weight: 500; +} + +/* 活动列表 - 毛玻璃卡片 */ +.activity-list { + padding: 0 32rpx; +} + +.activity-card { + margin-bottom: 32rpx; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(16rpx); + border-radius: 32rpx; + overflow: hidden; + box-shadow: 0 8rpx 32rpx rgba(76, 175, 80, 0.12), + 0 4rpx 16rpx rgba(76, 175, 80, 0.08); + border: 1rpx solid rgba(76, 175, 80, 0.15); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.activity-card:active { + transform: scale(0.98); + box-shadow: 0 4rpx 16rpx rgba(76, 175, 80, 0.15); +} + +/* 活动图片容器 */ +.activity-image-wrap { + position: relative; + width: 100%; + height: 360rpx; + overflow: hidden; + background: linear-gradient(135deg, #E8F5E9 0%, #F1F8F4 100%); +} + +.activity-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* 点赞徽章 */ +.like-badge { + position: absolute; + top: 24rpx; + right: 24rpx; + display: flex; + align-items: center; + gap: 8rpx; + padding: 10rpx 20rpx; + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(10rpx); + border-radius: 100rpx; + z-index: 10; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); +} + +.like-icon { + width: 32rpx; + height: 32rpx; +} + +.like-count { + font-size: 24rpx; + color: #4A5565; + font-weight: 600; +} + +.like-badge.liked .like-count { + color: #FF5252; +} + +/* 价格标签 */ +.price-tag { + position: absolute; + bottom: 24rpx; + left: 24rpx; + padding: 10rpx 24rpx; + border-radius: 12rpx; + font-size: 24rpx; + font-weight: 700; + color: #FFFFFF; + z-index: 10; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15); +} + +.price-tag.free { + background: #4CAF50; +} + +.price-tag.paid { + background: #FF9800; +} + +.location-badge { + position: absolute; + top: 24rpx; + left: 24rpx; + padding: 12rpx 24rpx; + background: rgba(27, 94, 32, 0.85); + backdrop-filter: blur(12rpx); + border-radius: 100rpx; + display: flex; + align-items: center; + gap: 8rpx; + font-size: 24rpx; + color: #FFFFFF; + font-weight: 500; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.15); +} + +.location-icon { + width: 24rpx; + height: 24rpx; +} + +/* 活动信息 */ +.activity-info { + padding: 40rpx; +} + +.activity-title { + font-size: 36rpx; + font-weight: 700; + color: #2E7D32; + margin-bottom: 20rpx; + line-height: 1.4; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; +} + +.activity-meta { + display: flex; + align-items: center; + gap: 32rpx; + margin-bottom: 24rpx; +} + +.meta-item { + display: flex; + align-items: center; + gap: 8rpx; + font-size: 26rpx; + color: #43A047; +} + +.meta-icon { + width: 28rpx; + height: 28rpx; +} + +.meta-text { + font-size: 26rpx; + color: #4A5565; +} + +.meta-row { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + margin-top: 8rpx; +} + +/* 活动底部 */ +.activity-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 24rpx; + border-top: 1rpx solid rgba(76, 175, 80, 0.1); +} + +.participants { + display: flex; + align-items: center; + gap: 12rpx; +} + +.avatar-stack { + display: flex; + align-items: center; +} + +.mini-avatar { + width: 48rpx; + height: 48rpx; + border-radius: 50%; + background: #E8F5E9; + border: 2rpx solid #fff; + margin-left: -12rpx; +} + +.mini-avatar:first-child { + margin-left: 0; +} + +.participant-text { + font-size: 24rpx; + color: #62748E; +} + +.heat-item { + margin-left: auto; +} + +.heat-text { + color: #FF9800; + font-weight: 600; +} + +/* 立即报名按钮 */ +.signup-btn { + width: 220rpx; + height: 72rpx; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #66BB6A 0%, #43A047 100%); + border-radius: 100rpx; + font-size: 28rpx; + font-weight: 700; + color: #FFFFFF; + box-shadow: 0 6rpx 20rpx rgba(76, 175, 80, 0.3); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + white-space: nowrap; + flex-shrink: 0; +} + +.signup-btn:active { + transform: scale(0.95); + box-shadow: 0 4rpx 12rpx rgba(76, 175, 80, 0.35); +} + +/* 空状态 */ +.empty-state { + padding: 120rpx 32rpx; + text-align: center; +} + +.empty-icon { + width: 200rpx; + height: 200rpx; + margin: 0 auto 32rpx; + opacity: 0.5; +} + +.empty-text { + font-size: 28rpx; + color: #81C784; +} + +/* 列表底部 */ +.list-footer { + padding: 40rpx 0; + text-align: center; +} + +.footer-text { + font-size: 24rpx; + color: #99A1AF; +} + +/* 二维码弹窗 */ +.qrcode-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + display: none; + align-items: center; + justify-content: center; +} + +.qrcode-modal.show { + display: flex; +} + +/* 遮罩层 */ +.modal-mask { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4rpx); +} + +/* 弹窗内容 */ +.modal-content { + position: relative; + width: 680rpx; + background: #FFFFFF; + border-radius: 64rpx; + padding: 64rpx; + box-shadow: 0 50rpx 100rpx -24rpx rgba(0, 0, 0, 0.25); + display: flex; + flex-direction: column; + align-items: center; + z-index: 1; +} + +/* 关闭按钮 */ +.close-btn { + position: absolute; + top: 32rpx; + right: 32rpx; + width: 72rpx; + height: 72rpx; + background: #F1F5F9; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.close-btn:active { + transform: scale(0.9); + background: #E2E8F0; +} + +.close-icon { + width: 40rpx; + height: 40rpx; +} + +/* 标题 */ +.modal-title { + font-size: 48rpx; + font-weight: 700; + color: #1D293D; + text-align: center; + margin-bottom: 16rpx; + line-height: 1.5; +} + +/* 副标题 */ +.modal-subtitle { + font-size: 32rpx; + color: #62748E; + text-align: center; + margin-bottom: 48rpx; + line-height: 1.5; +} + +/* 二维码容器 */ +.qrcode-container { + width: 440rpx; + height: 440rpx; + background: #F8FAFC; + border: 2rpx solid #F1F5F9; + border-radius: 40rpx; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 48rpx; + overflow: hidden; +} + +.qrcode-image { + width: 404rpx; + height: 404rpx; + border-radius: 24rpx; +} + +/* 保存按钮 */ +.save-btn { + width: 552rpx; + height: 116rpx; + background: #07C160; + border-radius: 100rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 40rpx; + font-weight: 700; + color: #FFFFFF; + box-shadow: 0 20rpx 30rpx -6rpx rgba(220, 252, 231, 1), + 0 8rpx 12rpx -8rpx rgba(220, 252, 231, 1); + transition: all 0.3s ease; +} + +.save-btn:active { + transform: scale(0.96); + box-shadow: 0 10rpx 20rpx -6rpx rgba(220, 252, 231, 1); +} diff --git a/pages/performance/performance.js b/pages/performance/performance.js new file mode 100644 index 0000000..0c6ebed --- /dev/null +++ b/pages/performance/performance.js @@ -0,0 +1,214 @@ +const api = require('../../utils/api') +const util = require('../../utils/util') + +Page({ + data: { + statusBarHeight: 20, + navHeight: 64, + loading: false, + list: [], + page: 1, + pageSize: 20, + hasMore: true, + individualPerformance: '0', + teamTotalPerformance: '0', + pendingAmount: '210.00', + levelName: '', + defaultAvatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=500&auto=format&fit=crop&q=60', + currentTab: 'individual' // 'individual' | 'team' + }, + + onLoad() { + console.log('[Performance] Page onLoad'); + this.loadStats() + this.loadRecords() + }, + + onShow() { + // 隐藏系统默认 TabBar + wx.hideTabBar({ animation: false }); + }, + + onPullDownRefresh() { + this.setData({ + page: 1, + hasMore: true, + list: [] + }, () => { + Promise.all([ + this.loadStats(), + this.loadRecords() + ]).then(() => { + wx.stopPullDownRefresh() + }) + }) + }, + + onReachBottom() { + if (this.data.loading || !this.data.hasMore) return + this.setData({ + page: this.data.page + 1 + }, () => { + this.loadRecords() + }) + }, + + async loadStats() { + try { + console.log('[Performance] Loading stats...'); + const res = await api.commission.getStats() + console.log('[Performance] Stats response:', res); + if (res.success && res.data) { + const data = res.data + // Mapping levels + const roleMap = { + 'soulmate': '心伴会员', + 'guardian': '守护会员', + 'companion': '陪伴会员', + 'listener': '倾听会员', + 'partner': '城市合伙人' + }; + + const individual = Number(data.individualPerformance || data.individual_performance || 0); + const team = Number(data.teamTotalPerformance || data.team_total_performance || 0); + + this.setData({ + individualPerformance: this.formatAmount(individual), + teamTotalPerformance: this.formatAmount(individual + team), // 总业绩 = 个人 + 团队 + pendingAmount: Number(data.pendingAmount || data.pending_amount || 0).toFixed(2), + totalCommission: Number(data.totalCommission || data.total_commission || 0).toFixed(2), + levelName: roleMap[data.distributorRole || data.role] || data.distributorRoleName || '分销会员', + isSoulmate: (data.distributorRole || data.role) === 'soulmate' + }) + } + } catch (err) { + console.error('加载统计失败', err) + } + }, + + /** + * 格式化金额,超过一万显示为“x.x万” + * @param {number|string} val + */ + formatAmount(val) { + const num = Number(val || 0); + if (num >= 10000) { + return (num / 10000).toFixed(1) + '万'; + } + return num.toFixed(2); + }, + + async loadRecords() { + if (this.data.loading) return + this.setData({ loading: true }) + + try { + console.log('[Performance] Loading records...', { page: this.data.page, scope: this.data.currentTab }); + const res = await api.commission.getRecords({ + page: this.data.page, + limit: this.data.pageSize, + scope: this.data.currentTab // Suggesting this parameter to backend + }) + console.log('[Performance] Records response:', res); + + if (res.success && res.data) { + const records = (res.data.list || res.data || []).map(record => this.transformRecord(record)) + + this.setData({ + list: this.data.page === 1 ? records : [...this.data.list, ...records], + hasMore: records.length === this.data.pageSize + }) + } + } catch (err) { + console.error('加载记录失败', err) + wx.showToast({ + title: '加载失败', + icon: 'none' + }) + } finally { + this.setData({ loading: false }) + } + }, + + switchTab(e) { + const tab = e.currentTarget.dataset.tab + if (tab === this.data.currentTab) return + + this.setData({ + currentTab: tab, + page: 1, + list: [], + hasMore: true + }, () => { + this.loadRecords() + }) + }, + + transformRecord(record) { + const dateObj = new Date(record.created_at || record.createdAt) + const fmtTime = util.formatTime(dateObj) + + let avatar = record.fromUserAvatar || record.userAvatar || record.avatar || ''; + if (avatar) { + avatar = util.getFullImageUrl(avatar); + } + + const roleMap = { + 'soulmate': '心伴会员', + 'guardian': '守护会员', + 'companion': '陪伴会员', + 'listener': '倾听会员', + 'partner': '城市合伙人' + }; + + // 优先使用API返回的中文等级名称 + const userLevel = record.fromUserRoleName || record.userRoleName || roleMap[record.fromUserRole] || roleMap[record.userRole] || record.levelText || '普通用户'; + + return { + id: record.id, + orderNo: record.orderNo || record.order_no || record.id || '---', + userName: record.fromUserName || record.userName || '匿名用户', + userAvatar: avatar || this.data.defaultAvatar, + productName: this.getOrderTypeText(record.orderType || record.type), + userLevel: userLevel, + orderAmount: record.orderAmount ? Number(record.orderAmount).toFixed(2) : (record.amount ? Number(record.amount).toFixed(2) : '0.00'), + time: fmtTime + } + }, + + onAvatarError(e) { + const index = e.currentTarget.dataset.index; + if (index !== undefined) { + const list = this.data.list; + list[index].userAvatar = '/images/default-avatar.svg'; + this.setData({ list }); + } + }, + + getOrderTypeText(type) { + const map = { + 'recharge': '充值', + 'vip': 'VIP会员', + 'identity_card': '身份卡', + 'agent_purchase': '智能体购买', + 'companion_chat': '陪聊' + } + return map[type] || '推广订单' + }, + + goTeam() { + wx.navigateTo({ + url: '/pages/team/team' + }) + }, + + onBack() { + wx.navigateBack({ + delta: 1, + fail: (err) => { + console.error('[Performance] Back failed, navigating to profile', err); + wx.switchTab({ url: '/pages/profile/profile' }); + } + }); + } +}) diff --git a/pages/performance/performance.json b/pages/performance/performance.json new file mode 100644 index 0000000..6dbede9 --- /dev/null +++ b/pages/performance/performance.json @@ -0,0 +1,6 @@ +{ + "usingComponents": { + "app-icon": "/components/icon/icon" + }, + "navigationBarTitleText": "业绩数据" +} \ No newline at end of file diff --git a/pages/performance/performance.wxml b/pages/performance/performance.wxml new file mode 100644 index 0000000..3b9e80b --- /dev/null +++ b/pages/performance/performance.wxml @@ -0,0 +1,85 @@ + + + + + + 返回 + + 业绩数据 + + + + + + + + + + + + {{levelName || '分销会员'}} + + + + + 个人业绩 + ¥{{individualPerformance || '0.00'}} + + + 总计业绩 + ¥{{teamTotalPerformance || '0.00'}} + + + + + + + + + + 个人业绩明细 + + + 团队业绩明细 + + + + + + 最近推广订单 + + + + + + {{item.userName}} + {{item.productName}} · {{item.userLevel}} + 时间: {{item.time}} + 单号: {{item.orderNo}} + + + + ¥{{item.orderAmount}} + + + + + + + 加载中... + + + 暂无推广订单 + + + 没有更多了 + + + + + + + 已显示全部数据 + + + diff --git a/pages/performance/performance.wxss b/pages/performance/performance.wxss new file mode 100644 index 0000000..ba03a8c --- /dev/null +++ b/pages/performance/performance.wxss @@ -0,0 +1,208 @@ +.page { + min-height: 100vh; + background: #E8C3D4; +} + +.container { + padding: 32rpx; +} + +/* 顶部紫色统计卡片 */ +.header-card { + background: linear-gradient(135deg, #CF91D3 0%, #B06AB3 100%); + border-radius: 40rpx; + padding: 48rpx; + color: #FFFFFF; + margin-bottom: 32rpx; + box-shadow: 0 12rpx 32rpx rgba(176, 106, 179, 0.3); +} + +.user-level-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 48rpx; +} + +.level-left { + display: flex; + align-items: center; + gap: 24rpx; +} + +.icon-bg { + width: 72rpx; + height: 72rpx; + background: rgba(255, 255, 255, 0.15); + border-radius: 20rpx; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(10rpx); +} + +.level-name { + font-size: 38rpx; + font-weight: 800; + letter-spacing: 2rpx; +} + +.stats-grid { + display: flex; + justify-content: space-between; +} + +.stat-box { + flex: 1; +} + +.stat-label { + font-size: 28rpx; + opacity: 0.85; + margin-bottom: 20rpx; + display: block; +} + +.stat-value { + font-size: 72rpx; + font-weight: 800; + letter-spacing: 2rpx; +} + +/* 白色内容卡片 */ +.content-card { + background: #FFFFFF; + border-radius: 40rpx; + padding: 32rpx; + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.05); + margin-bottom: 32rpx; +} + +.detail-tabs { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 32rpx; + padding-bottom: 24rpx; + border-bottom: 1rpx solid #F3F4F6; +} + +.tab-item { + display: flex; + align-items: center; + gap: 12rpx; + padding: 10rpx 0; +} + +.tab-item .tab-text { + font-size: 30rpx; + color: #9CA3AF; + font-weight: 500; + transition: all 0.3s; +} + +.tab-item.active .tab-text { + font-size: 32rpx; + color: #B06AB3; + font-weight: 800; +} + +/* 订单部分 */ +.orders-section { + padding: 0; +} + +.section-title { + font-size: 30rpx; + font-weight: 800; + color: #111827; + margin-bottom: 32rpx; +} + +.order-list { + display: flex; + flex-direction: column; + gap: 24rpx; +} + +.order-item { + background: #F9FAFB; + border-radius: 28rpx; + padding: 32rpx; + display: flex; + align-items: center; + border: 2rpx solid transparent; +} + +.order-item:active { + background: #F3F4F6; +} + +.user-avatar { + width: 100rpx; + height: 100rpx; + border-radius: 24rpx; /* Matches Figma's 14px/16px border radius feel */ + background: #E5E7EB; + margin-right: 24rpx; +} + +.order-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 4rpx; +} + +.user-name { + font-size: 30rpx; + font-weight: 800; + color: #111827; +} + +.product-info { + font-size: 24rpx; + color: #9CA3AF; + font-weight: 500; +} + +.order-no { + font-size: 20rpx; + color: #D1D5DB; + font-weight: 400; + margin-top: 4rpx; +} + +.order-right { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4rpx; +} + +.label { + font-size: 22rpx; + color: #9CA3AF; + font-weight: 600; +} + +.amount { + font-size: 34rpx; + font-weight: 900; + color: #B06AB3; +} + +/* 底部提示 */ +.bottom-tip { + text-align: center; + padding: 40rpx 0; + font-size: 26rpx; + color: #9CA3AF; +} + +/* 状态样式 */ +.loading-status, .empty-status, .no-more { + text-align: center; + padding: 60rpx 0; + font-size: 26rpx; + color: #9CA3AF; +} diff --git a/pages/profile/profile.js b/pages/profile/profile.js new file mode 100644 index 0000000..1b8e888 --- /dev/null +++ b/pages/profile/profile.js @@ -0,0 +1,640 @@ +const api = require('../../utils/api'); +const util = require('../../utils/util'); +const app = getApp(); + +Page({ + data: { + defaultAvatar: '/images/default-avatar.svg', + me: { + id: '', + idShort: '', + nickname: '未登录', + avatar: '', + phone: '' + }, + vip: { + levelText: '', + expireText: '' + }, + balances: { + grass: 0, + commission: '0.00' + }, + counts: { + orders: 0, + team: 0, + performance: 0 + }, + isLoggedIn: false, + isDistributor: false, + referralCode: '', + statusBarHeight: 20, + totalUnread: 0, + + // Modal States + showPromoterModal: false, + showPromoterSuccess: false, + + // 注册奖励 + showRegistrationReward: false, + registrationRewardAmount: 0, + claiming: false, + auditStatus: 0, + + // GF100 弹窗 + showGf100Popup: false, + gf100ImageUrl: '' + }, + + onShow() { + this.setData({ + statusBarHeight: wx.getSystemInfoSync().statusBarHeight, + auditStatus: app.globalData.auditStatus + }); + wx.hideTabBar({ animation: false }); + this.loadAll(); + }, + + async loadAll() { + const isLoggedIn = app.globalData.isLoggedIn || !!wx.getStorageSync('auth_token'); + this.setData({ isLoggedIn }); + + wx.showNavigationBarLoading(); + try { + if (isLoggedIn) { + await Promise.all([ + this.loadMe(), + this.loadBalance(), + this.loadCommission(), + this.loadCounts(), + this.loadUnreadCount() + ]); + this.checkRegistrationReward(); + this.checkGf100Popup(); + } else { + this.setData({ + me: { nickname: '未登录', avatar: this.data.defaultAvatar }, + balances: { grass: 0, commission: '0.00' }, + counts: { orders: 0, team: 0, performance: 0 }, + totalUnread: 0 + }); + } + } catch (err) { + console.error('Load profile failed', err); + } finally { + wx.hideNavigationBarLoading(); + } + }, + + /** + * 检查是否需要登录 + * @returns {boolean} 是否已登录 + */ + requireLogin() { + const isLoggedIn = app.globalData.isLoggedIn || !!wx.getStorageSync('auth_token'); + if (!isLoggedIn) { + wx.showModal({ + title: '提示', + content: '请先登录后再操作', + confirmText: '去登录', + success: (res) => { + if (res.confirm) { + wx.navigateTo({ + url: '/pages/login/login' + }); + } + } + }); + return false; + } + return true; + }, + + async loadMe() { + try { + // Use direct request with cache busting to ensure fresh data + const res = await api.request('/auth/me', { data: { _t: Date.now() } }); + // Correctly unwrap the data object from the response + const user = (res && res.data) ? res.data : {}; + const id = user.id || ''; + const idShort = id ? String(id).substring(0, 8).toUpperCase() : ''; + const nickname = user.nickname || user.name || '微信用户'; + + // 处理头像:如果是默认头像则不处理,否则拼接完整路径 + let avatar = user.avatar || user.avatar_url; + if (avatar) { + avatar = util.getFullImageUrl(avatar); + } else { + avatar = this.data.defaultAvatar; + } + + const phone = user.phone || user.mobile || ''; + + // Determine Display Role + const distributorRole = user.distributorRole || user.role; + let roleText = ''; + let roleClass = ''; + + const roleMap = { + 'soulmate': { text: '心伴会员', class: 'vip-soulmate' }, + 'guardian': { text: '守护会员', class: 'vip-guardian' }, + 'companion': { text: '陪伴会员', class: 'vip-companion' }, + 'listener': { text: '倾听会员', class: 'vip-listener' }, + 'partner': { text: '城市合伙人', class: 'vip-partner' } + }; + + if (user.isDistributor && roleMap[distributorRole]) { + const info = roleMap[distributorRole]; + roleText = info.text; + roleClass = info.class; + } else { + // Fallback to VIP + const vipLevel = Number(user.vip_level || 0); + if (vipLevel > 0) { + roleText = vipLevel >= 2 ? 'SVIP' : 'VIP'; + roleClass = 'vip-normal'; + } + } + + this.setData({ + me: { id, idShort, nickname, avatar, phone }, + vip: { levelText: roleText || '', levelClass: roleClass }, + isDistributor: !!user.isDistributor + }); + + // CRITICAL: Update global data and local storage to ensure avatar consistency across pages + if (app && app.setUserInfo) { + app.setUserInfo(user); + } else { + // Fallback if app.setUserInfo is not available + app.globalData.userInfo = user; + app.globalData.userId = user.id; + wx.setStorageSync('user_info', user); + } + } catch (e) { + console.error('loadMe error', e); + } + }, + + async loadBalance() { + try { + const res = await api.user.getBalance(); + const data = (res && res.data) ? res.data : res; + const balance = data.flower_balance || data.balance || 0; + this.setData({ + 'balances.grass': Number(balance) + }); + } catch (e) { + console.error('loadBalance error', e); + } + }, + + async loadCommission() { + try { + const res = await api.commission.getStats(); + const data = (res && res.data) ? res.data : res; + const commission = data.commissionBalance || data.commission_balance || 0; + // Use individualPerformance for "Performance Data" card as per documentation + const performance = data.individualPerformance || data.individual_performance || data.totalContribution || data.total_contribution || 0; + + this.setData({ + 'balances.commission': Number(commission).toFixed(2), + 'counts.performance': this.formatAmount(performance) + }); + } catch (e) { + console.error('loadCommission error', e); + } + }, + + /** + * 格式化金额,超过一万显示为“x.x万” + * @param {number|string} val + */ + formatAmount(val) { + const num = Number(val || 0); + if (num >= 10000) { + return (num / 10000).toFixed(1) + '万'; + } + return num.toFixed(2); + }, + + async loadCounts() { + try { + // 1. Fetch self orders (all statuses) + const selfOrdersRes = await api.payment.getOrders({ page: 1, pageSize: 1 }); + const selfCount = (selfOrdersRes.data && selfOrdersRes.data.total) ? selfOrdersRes.data.total : 0; + + // 2. Fetch promotion orders (records) - usually pageSize=1 is enough to get total + // Note: check backend response structure for total count + const promoRes = await api.commission.getRecords({ page: 1, pageSize: 1 }); + const promoCount = (promoRes.data && promoRes.data.total) ? promoRes.data.total : 0; + + // 3. Fetch team members count + const teamRes = await api.commission.getReferrals({ page: 1, pageSize: 1 }); + const teamCount = (teamRes.data && teamRes.data.total) ? teamRes.data.total : 0; + + this.setData({ + 'counts.orders': selfCount + promoCount, + 'counts.team': teamCount + }); + } catch (err) { + console.error('loadCounts failed', err); + // Keep existing data or set to 0 on error + // this.setData({ 'counts.orders': 0, 'counts.team': 0 }); + } + }, + + async loadUnreadCount() { + if (!this.data.isLoggedIn) { + this.setData({ totalUnread: 0 }); + return; + } + + try { + const [convRes, proactiveRes] = await Promise.all([ + api.chat.getConversations(), + api.proactiveMessage.getPending ? api.proactiveMessage.getPending() : Promise.resolve({ success: true, data: [] }) + ]); + + let totalUnread = 0; + if (convRes.success && convRes.data) { + totalUnread = convRes.data.reduce((sum, conv) => sum + (conv.unread_count || 0), 0); + } + + if (proactiveRes.success && proactiveRes.data && Array.isArray(proactiveRes.data)) { + totalUnread += proactiveRes.data.length; + } + + this.setData({ totalUnread }); + } catch (err) { + console.log('获取未读消息数失败', err); + this.setData({ totalUnread: 0 }); + } + }, + + goSettings() { + if (this.requireLogin()) { + wx.navigateTo({ url: '/pages/settings/settings' }); + } + }, + goEdit() { + if (this.requireLogin()) { + wx.navigateTo({ url: '/pages/edit-profile/edit-profile' }); + } + }, + goRecharge() { + if (this.requireLogin()) { + wx.navigateTo({ url: '/pages/recharge/recharge' }); + } + }, + goOrders() { + if (this.requireLogin()) { + wx.navigateTo({ url: '/pages/orders/orders' }); + } + }, + goTeam() { + if (this.requireLogin()) { + wx.navigateTo({ url: '/pages/team/team' }); + } + }, + goGiftShop() { + if (this.requireLogin()) { + wx.navigateTo({ url: '/pages/gift-shop/gift-shop' }); + } + }, + goBackpack() { + if (this.requireLogin()) { + wx.navigateTo({ url: '/pages/backpack/backpack' }); + } + }, + goMyActivities() { + if (this.requireLogin()) { + wx.navigateTo({ url: '/pages/my-activities/my-activities' }); + } + }, + goWithdraw() { + if (this.requireLogin()) { + wx.navigateTo({ url: '/pages/withdraw/withdraw' }); + } + }, + goCommission() { + if (this.requireLogin()) { + wx.navigateTo({ url: '/pages/commission/commission' }); + } + }, + goPerformance() { + if (this.requireLogin()) { + wx.navigateTo({ url: '/pages/performance/performance' }); + } + }, + goCooperation() { + if (this.requireLogin()) { + wx.navigateTo({ url: '/subpackages/cooperation/pages/cooperation/cooperation' }); + } + }, + goSupport() { + if (this.requireLogin()) { + wx.navigateTo({ url: '/pages/support/support' }); + } + }, + goPromote() { + if (this.requireLogin()) { + wx.navigateTo({ url: '/pages/promote/promote' }); + } + }, + + onAvatarError() { + this.setData({ + 'me.avatar': this.data.defaultAvatar + }); + }, + + async handleLogout() { + wx.showModal({ + title: '提示', + content: '确定要退出登录吗?', + success: async (res) => { + if (res.confirm) { + try { + await api.auth.logout(); + } catch (e) { + console.error('logout api error', e); + } + + wx.removeStorageSync('auth_token'); + wx.removeStorageSync('user_info'); + + app.globalData.isLoggedIn = false; + app.globalData.userInfo = null; + + this.loadAll(); + + wx.showToast({ + title: '已退出登录', + icon: 'success' + }); + } + } + }); + }, + + /** + * 检查注册奖励领取资格 + */ + async checkRegistrationReward() { + try { + if (!api.lovePoints || typeof api.lovePoints.checkRegistrationReward !== 'function') { + return; + } + // 检查注册奖励领取资格 - 增加静默处理,避免 404 时控制台报错 + const res = await api.lovePoints.checkRegistrationReward(); + if (res && res.success && res.data && res.data.eligible) { + this.setData({ + showRegistrationReward: true, + registrationRewardAmount: res.data.amount || 100 + }); + } + } catch (err) { + // 生产环境可能未部署此接口,静默处理 + if (err.code !== 404) { + console.warn('[profile] 检查注册奖励静默失败:', err.message || '接口可能未部署'); + } + } + }, + + /** + * 领取注册奖励 + */ + async onClaimReward() { + if (this.data.claiming) return; + + if (!api.lovePoints || typeof api.lovePoints.claimRegistrationReward !== 'function') { + wx.showToast({ title: '功能暂不可用', icon: 'none' }); + return; + } + + this.setData({ claiming: true }); + wx.showLoading({ title: '领取中...' }); + + try { + const res = await api.lovePoints.claimRegistrationReward(); + wx.hideLoading(); + + if (res.success) { + wx.showToast({ + title: '领取成功', + icon: 'success', + duration: 2000 + }); + + this.setData({ + showRegistrationReward: false + }); + + // 如果后端返回了免费畅聊时间,可以做提示 + if (res.data && res.data.free_chat_time) { + console.log('[profile] 获得免费畅聊时间:', res.data.free_chat_time); + setTimeout(() => { + wx.showModal({ + title: '额外奖励', + content: '恭喜获得 60 分钟免费畅聊时间,现在就去和 AI 角色聊天吧!', + confirmText: '去聊天', + success: (modalRes) => { + if (modalRes.confirm) { + wx.switchTab({ url: '/pages/index/index' }); + } + } + }); + }, 2000); + } + + // 刷新余额 + this.loadBalance(); + } else { + wx.showToast({ + title: res.message || '领取失败', + icon: 'none' + }); + } + } catch (err) { + wx.hideLoading(); + console.error('[profile] 领取注册奖励失败:', err); + wx.showToast({ + title: '网络错误,请重试', + icon: 'none' + }); + } finally { + this.setData({ claiming: false }); + } + }, + + /** + * 关闭注册奖励弹窗 + */ + closeRewardPopup() { + this.setData({ + showRegistrationReward: false + }); + }, + + /** + * 检查 GF100 弹窗状态 + */ + async checkGf100Popup() { + if (!this.data.isLoggedIn) return; + + try { + const res = await api.lovePoints.checkGf100Status(); + console.log('[profile] GF100 检查结果:', res); + if (res.success && res.data && res.data.showPopup) { + const imageUrl = res.data.imageUrl; + this.setData({ + showGf100Popup: true, + gf100ImageUrl: imageUrl ? util.getFullImageUrl(imageUrl) : '/images/gf100.png' + }); + } + } catch (err) { + console.error('[profile] 检查 GF100 弹窗失败:', err); + } + }, + + /** + * 领取 GF100 奖励 + */ + async onClaimGf100() { + if (this.data.claiming) return; + + this.setData({ claiming: true }); + wx.showLoading({ title: '领取中...', mask: true }); + + try { + const res = await api.lovePoints.claimGf100(); + wx.hideLoading(); + + if (res.success) { + wx.showToast({ + title: '领取成功', + icon: 'success', + duration: 2000 + }); + + this.setData({ + showGf100Popup: false + }); + + // 弹出获得 60 分钟免费畅聊的提示 + setTimeout(() => { + wx.showModal({ + title: '领取成功', + content: '恭喜获得 100 爱心 + 60 分钟免费畅聊时间!', + confirmText: '去聊天', + success: (modalRes) => { + if (modalRes.confirm) { + wx.switchTab({ url: '/pages/chat/chat' }) + } + } + }); + }, 500); + + // 刷新余额 + this.loadBalance(); + } else { + wx.showToast({ + title: res.message || '领取失败', + icon: 'none' + }); + } + } catch (err) { + wx.hideLoading(); + console.error('[profile] 领取 GF100 失败:', err); + wx.showToast({ + title: '网络错误,请重试', + icon: 'none' + }); + } finally { + this.setData({ claiming: false }); + } + }, + + /** + * 关闭 GF100 弹窗 + */ + closeGf100Popup() { + this.setData({ + showGf100Popup: false + }); + }, + + preventBubble() { }, + preventTouchMove() { }, + + async onLoad() { + await this.loadReferralCode() + }, + + async loadReferralCode() { + if (!this.data.isLoggedIn) return + try { + const res = await api.commission.getStats() + if (res.success && res.data) { + this.setData({ referralCode: res.data.referralCode || '' }) + } + } catch (err) { + console.error('加载推荐码失败:', err) + } + }, + + onShareAppMessage() { + const { referralCode, isDistributor } = this.data + const referralCodeParam = referralCode ? `?referralCode=${referralCode}` : '' + + this.recordShareReward() + + return { + title: isDistributor ? `我的推荐码:${referralCode},注册即可享受优惠!` : '欢迎来到心伴俱乐部 - 随时可聊 一直陪伴', + path: `/pages/index/index${referralCodeParam}`, + imageUrl: isDistributor ? '/images/share-commission.png' : '/images/icon-heart-new.png' + } + }, + + onShareTimeline() { + const { referralCode, isDistributor } = this.data + const query = referralCode ? `referralCode=${referralCode}` : '' + + this.recordShareReward() + + return { + title: isDistributor ? `推荐码:${referralCode},注册即可享受优惠!` : '心伴俱乐部 - 随时可聊 一直陪伴', + query: query, + imageUrl: isDistributor ? '/images/share-commission.png' : '/images/icon-heart-new.png' + } + }, + + /** + * 静默记录分享奖励(分享人A获得+100爱心值) + */ + async recordShareReward() { + try { + const res = await api.lovePoints.share() + console.log('[profile] 分享爱心值奖励:', res) + } catch (err) { + console.error('[profile] 记录分享奖励失败:', err) + } + }, + + // Tab Bar Switching + switchTab(e) { + const path = e.currentTarget.dataset.path; + const app = getApp(); + + if (path === '/pages/chat/chat') { + if (!app.globalData.isLoggedIn && !wx.getStorageSync('auth_token')) { + wx.navigateTo({ + url: '/pages/login/login?redirect=' + encodeURIComponent(path) + }); + return; + } + } + wx.switchTab({ url: path }); + } +}); diff --git a/pages/profile/profile.json b/pages/profile/profile.json new file mode 100644 index 0000000..3153ca5 --- /dev/null +++ b/pages/profile/profile.json @@ -0,0 +1,5 @@ +{ + "usingComponents": { + "app-icon": "/components/icon/icon" + } +} diff --git a/pages/profile/profile.wxml b/pages/profile/profile.wxml new file mode 100644 index 0000000..06f8d16 --- /dev/null +++ b/pages/profile/profile.wxml @@ -0,0 +1,326 @@ + + + + + 我的 + + + + + + + + + + + + {{me.nickname}} + + + {{vip.levelText}} + + + + + + {{me.phone}} + + + + + + + + + + 我的钱包 + + + + + + + + 我的会员 + + 心伴会员 + + + + + + + + + + 我的收益 + + 佣金明细 + + + + + + + 可提现 (元) + {{balances.commission}} + + + + + + + + + + + + + 客户管理 + + + + + + + + + + 我的团队 + + + {{counts.team}} + + + + + + + + + + + 业绩数据 + + + ¥{{counts.performance || '0.00'}} + + + + + + + + + + + + + 推广中心 + + + + + + + + + + 我的订单 + + + + + + + + + + + 我的活动 + + + + + + + + + + + 合作入驻 + + + + + + + + + + 在线客服 + + + + + + + + + + 修改资料 + + + + + + + + + + 礼品商城 + + + 去兑换 + + + + + + + + + + 退出登录 + + + + + + + + + + + 陪伴 + + + + 文娱 + + + + 服务 + + + + + + {{totalUnread}} + 99+ + + + 消息 + + + + 我的 + + + + + + + + + + + + × + + + + + + + + + + + + + + + + 我的活动 + 查看并管理您的报名记录 + + + + + 1 + + 高额佣金回报 + 每笔订单最高享 70% 分成 + + + + + 2 + + 专属身份标识 + 获得“推广合伙人”专属徽章 + + + + + 3 + + 极速提现权益 + 佣金T+1日结算,快速到账 + + + + + + + + + + + + + + + + 我的活动列表 + 正在加载您的活动信息... + + + + + + + + + + + + + + 您的活动申请正在处理中 + 如有疑问请联系客服查询 + + + + + + + diff --git a/pages/profile/profile.wxss b/pages/profile/profile.wxss new file mode 100644 index 0000000..4ea4400 --- /dev/null +++ b/pages/profile/profile.wxss @@ -0,0 +1,783 @@ +page { + background: #E8C3D4; + min-height: 100vh; + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; +} + +.page { + padding-top: 194rpx; + padding-bottom: 200rpx; /* Extra space for TabBar */ +} + +/* Custom Nav已移除,改用全局 unified-header */ + +.btn-reset { + padding: 0; + margin: 0; + line-height: inherit; + background: transparent; + border-radius: 0; + text-align: center; + width: auto !important; + min-width: 0 !important; +} +.btn-reset::after { border: none; } + +/* Common Card Styles */ +.section-wrapper { + padding: 0 32rpx; + margin-bottom: 32rpx; +} + +.white-card { + background: #FFFFFF; + border-radius: 48rpx; + padding: 42rpx; + box-shadow: 0 2rpx 4rpx -2rpx rgba(0, 0, 0, 0.1), 0 2rpx 6rpx 0 rgba(0, 0, 0, 0.1); + border: 2rpx solid #F9FAFB; +} + +.main-title { + font-size: 48rpx; + font-weight: 700; + color: #101828; +} + +.card-title-row { + margin-bottom: 32rpx; + display: flex; + justify-content: space-between; + align-items: center; +} + +.border-bottom { + border-bottom: 2rpx solid #F9FAFB; + padding-bottom: 2rpx; + margin-bottom: 40rpx; +} + +/* Header Profile */ +.profile-outer { + padding: 16rpx 32rpx 32rpx; +} + +.profile-card { + background: #FFFFFF; + border-radius: 48rpx; + padding: 40rpx; + display: flex; + align-items: center; + gap: 32rpx; + box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.02); + border: 2rpx solid #F9FAFB; +} + +.avatar-wrap { + width: 140rpx; + height: 140rpx; + border-radius: 50%; + border: 4rpx solid #F3D1EA; + padding: 4rpx; + flex-shrink: 0; +} + +.avatar { + width: 100%; + height: 100%; + border-radius: 50%; +} + +.profile-info { + flex: 1; + overflow: hidden; +} + +.name-row { + display: flex; + align-items: center; + gap: 24rpx; + margin-bottom: 16rpx; +} + +.nickname { + font-size: 40rpx; + font-weight: 900; + color: #111827; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.vip-badge { + background: linear-gradient(90deg, #B06AB3, #8E44AD); /* Default/fallback */ + padding: 4rpx 20rpx; + border-radius: 999rpx; + display: flex; + align-items: center; + gap: 8rpx; + box-shadow: 0 2rpx 4rpx rgba(0,0,0,0.1); +} + +.vip-badge.vip-soulmate { + background: linear-gradient(90deg, #F472B6, #991B1B); /* Pink to Deep Red */ +} + +.vip-badge.vip-guardian { + background: linear-gradient(90deg, #FFD700, #DAA520); /* Gold */ +} + +.vip-badge.vip-companion { + background: linear-gradient(90deg, #9B59B6, #8E44AD); /* Purple (Similar to main, maybe slightly distinct) -> Let's try Deep Blue/Purple */ + background: linear-gradient(90deg, #6C5CE7, #a29bfe); /* Soft Royal Blue/Purple */ +} + +.vip-badge.vip-listener { + background: linear-gradient(90deg, #2ecc71, #27ae60); /* Green */ +} + +.vip-badge.vip-partner { + background: linear-gradient(90deg, #E84393, #D63031); /* Pink/Red */ +} + +.vip-badge.vip-normal { + background: linear-gradient(90deg, #B06AB3, #8E44AD); /* Standard VIP Color */ +} + +.vip-text { + color: #FFFFFF; + font-size: 24rpx; + font-weight: 700; +} + +.id-row { + display: flex; + align-items: center; + gap: 12rpx; + color: #9CA3AF; + font-size: 28rpx; + font-weight: 500; +} + +/* Wallet Section Specifics */ +.wallet-stack { + display: flex; + flex-direction: column; + gap: 24rpx; +} + +.inner-card { + border-radius: 32rpx; + padding: 40rpx; + position: relative; + overflow: hidden; + width: 100%; + box-sizing: border-box; +} + +/* VIP Inner Card */ +.vip-card { + background: linear-gradient(180deg, #EEB8DD, #D489BE); + box-shadow: 0 10rpx 15rpx -3rpx rgba(238, 184, 221, 0.3); + display: flex; + align-items: center; + justify-content: space-between; + height: 200rpx; + overflow: hidden; +} + +.vip-content { + position: relative; + z-index: 2; +} + +.vip-header-row { + display: flex; + align-items: center; + gap: 16rpx; + margin-bottom: 4rpx; + opacity: 0.95; +} + +.vip-label { + color: #FFFFFF; + font-size: 28rpx; + font-weight: 700; +} + +.vip-main-text { + color: #FFFFFF; + font-size: 44rpx; + font-weight: 900; + letter-spacing: -1rpx; +} + +.vip-action-btn { + background: rgba(0, 0, 0, 0.15); + color: #FFFFFF; + padding: 16rpx 0; + border-radius: 999rpx; + font-size: 28rpx; + font-weight: 800; + box-shadow: 0 2rpx 4rpx rgba(0,0,0,0.1); + position: relative; + z-index: 2; + width: 160rpx !important; + margin-left: auto; + margin-right: 0 !important; +} + +.vip-card::after { + content: ''; + position: absolute; + bottom: -20rpx; + right: -20rpx; + width: 140rpx; + height: 140rpx; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9H4.5a2.5 2.5 0 0 1 0-5H6'/%3E%3Cpath d='M18 9h1.5a2.5 2.5 0 0 0 0-5H18'/%3E%3Cpath d='M4 22h16'/%3E%3Cpath d='M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22'/%3E%3Cpath d='M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22'/%3E%3Cpath d='M18 2H6v7a6 6 0 0 0 12 0V2Z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-size: contain; + opacity: 0.15; + transform: rotate(-15deg); + pointer-events: none; +} + +/* Earnings Inner Card */ +.earnings-card { + background: #FFFFFF; + border: 2rpx solid #F3F4F6; +} + +.earnings-header-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 32rpx; +} + +.earnings-title { + font-size: 32rpx; + font-weight: 700; + color: #111827; +} + +.commission-btn { + background: #FDF4F9; + padding: 12rpx 24rpx; + border-radius: 999rpx; + display: flex; + align-items: center; + gap: 4rpx; + color: #B06AB3; + font-size: 26rpx; + font-weight: 700; +} + +.earnings-body-row { + display: flex; + justify-content: space-between; + align-items: flex-end; + width: 100%; +} + +.earnings-info { + display: flex; + flex-direction: column; +} + +.earnings-label { + font-size: 24rpx; + color: #9CA3AF; + font-weight: 500; + margin-bottom: 8rpx; +} + +.earnings-amount { + font-size: 60rpx; + font-weight: 900; + color: #B06AB3; + line-height: 1; +} + +.withdraw-btn { + background: linear-gradient(135deg, #B06AB3 0%, #9B4D9E 100%); + color: #FFFFFF; + height: 72rpx; + border-radius: 999rpx; + font-size: 28rpx; + font-weight: 800; + box-shadow: 0 16rpx 32rpx rgba(176, 106, 179, 0.25); + width: 160rpx !important; + margin-left: auto; + margin-right: 0 !important; + display: flex; + align-items: center; + justify-content: center; +} + +/* Stats Grid New Design */ +.stats-grid { + display: flex; + width: 100%; + box-sizing: border-box; +} + +.stats-grid .stat-card + .stat-card { + margin-left: 32rpx; +} + +.stat-card { + flex: 1; + min-width: 0; + border-radius: 32rpx; + padding: 42rpx 42rpx 36rpx; + background: linear-gradient(180deg, #FDF4F9 0%, #FFF0F5 100%); + border: 2rpx solid rgba(243, 209, 234, 0.5); + position: relative; + box-sizing: border-box; +} + +.card-top { + display: flex; + align-items: center; + gap: 3rpx; + margin-bottom: 14rpx; +} + +.icon-wrap-sm { + width: 50rpx; + height: 50rpx; + background: rgba(255, 255, 255, 0.8); + border-radius: 28rpx; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2rpx 4rpx -2rpx rgba(0, 0, 0, 0.1), 0 2rpx 6rpx 0 rgba(0, 0, 0, 0.1); +} + +.card-name { + font-size: 36rpx; + font-weight: 900; + color: #101828; +} + +.card-bottom { + display: flex; + align-items: baseline; + gap: 8rpx; +} + +.card-num { + font-size: 50rpx; + font-weight: 900; + color: #B06AB3; + line-height: 1; +} + +.card-unit { + font-size: 28rpx; + font-weight: 700; + color: #6A7282; +} + +.card-icon-img { + width: 40rpx; + height: 40rpx; +} + +/* Menu List */ +.menu-card { + padding: 24rpx 0; +} + +.menu-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 32rpx 48rpx; +} + +.menu-divider { + height: 2rpx; + background: #F9FAFB; + margin: 0 40rpx; +} + +.menu-left { + display: flex; + align-items: center; + gap: 32rpx; +} + +.menu-text { + font-size: 32rpx; + font-weight: 700; + color: #374151; +} + +.menu-right { + display: flex; + align-items: center; + gap: 16rpx; +} + +.tag-pink { + background: #FDF4F9; + color: #B06AB3; + font-size: 24rpx; + font-weight: 700; + padding: 8rpx 24rpx; + border-radius: 999rpx; +} + +.safe-bottom-spacer { + height: 200rpx; +} + +/* 自定义底部导航栏 */ +.custom-tabbar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 194rpx; + background: #fff; + display: flex; + align-items: flex-start; + justify-content: space-around; + padding-top: 24rpx; + z-index: 999; + border-top: 2rpx solid #F3F4F6; +} + +.tabbar-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12rpx; + width: 150rpx; + height: 120rpx; +} + +.tabbar-icon { + width: 68rpx; + height: 68rpx; +} + +.tabbar-text { + font-family: Arial, sans-serif; + font-size: 40rpx; + font-weight: 700; + color: #A58AA5; + line-height: 1; +} + +.tabbar-text.active { + color: #B06AB3; +} + +.message-icon-wrapper { + position: relative; + width: 68rpx; + height: 68rpx; +} + +.message-icon-wrapper .tabbar-icon { + width: 68rpx; + height: 68rpx; +} + +.message-badge { + position: absolute; + top: -10rpx; + right: -16rpx; + min-width: 36rpx; + height: 36rpx; + padding: 0 8rpx; + background: #FB2C36; + border: 3rpx solid #fff; + border-radius: 18rpx; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; +} + +.message-badge text { + font-size: 22rpx; + font-weight: 600; + color: #fff; + line-height: 1; +} + +/* ==================== 弹窗样式 ==================== */ +.modal-mask { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.6); + backdrop-filter: blur(8rpx); + z-index: 2000; + display: flex; + align-items: center; + justify-content: center; + padding: 40rpx; +} + +.promoter-modal { + width: 100%; + max-width: 640rpx; + background: #FFFFFF; + border-radius: 48rpx; + padding: 48rpx; + position: relative; + animation: modalPop 0.3s cubic-bezier(0.16, 1, 0.3, 1); +} + +@keyframes modalPop { + from { opacity: 0; transform: scale(0.95); } + to { opacity: 1; transform: scale(1); } +} + +.modal-close-btn { + position: absolute; + top: 32rpx; + right: 32rpx; + padding: 16rpx; + z-index: 10; +} + +.modal-header { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 48rpx; +} + +.modal-icon-bg { + width: 128rpx; + height: 128rpx; + background: #FDF4F9; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 32rpx; +} + +.modal-title { + font-size: 40rpx; + font-weight: 900; + color: #111827; + margin-bottom: 12rpx; +} + +.modal-subtitle { + font-size: 28rpx; + color: #6B7280; + font-weight: 600; +} + +.benefits-list { + display: flex; + flex-direction: column; + gap: 24rpx; + margin-bottom: 48rpx; +} + +.benefit-item { + display: flex; + align-items: flex-start; + gap: 24rpx; + padding: 32rpx; + background: #F9FAFB; + border-radius: 32rpx; + border: 2rpx solid #F3F4F6; +} + +.benefit-num { + width: 48rpx; + height: 48rpx; + background: #B06AB3; + color: #FFFFFF; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + font-weight: 800; + flex-shrink: 0; + margin-top: 4rpx; +} + +.benefit-info { + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.benefit-title { + font-size: 32rpx; + font-weight: 800; + color: #111827; +} + +.benefit-desc { + font-size: 28rpx; + color: #4B5563; + line-height: 1.4; +} + +.apply-btn { + width: 100%; + background: #B06AB3; + padding: 32rpx 0; + border-radius: 32rpx; + display: flex; + flex-direction: column; + align-items: center; + gap: 8rpx; + box-shadow: 0 16rpx 32rpx rgba(176, 106, 179, 0.25); +} + +.apply-text { + font-size: 34rpx; + font-weight: 800; + color: #FFFFFF; + letter-spacing: 2rpx; +} + +.apply-subtext { + font-size: 24rpx; + color: rgba(255,255,255,0.9); + font-weight: 600; +} + +/* Success Modal Specifics */ +.success-modal .qr-container { + display: flex; + justify-content: center; + margin-bottom: 32rpx; +} + +.qr-placeholder { + padding: 32rpx; + border: 8rpx solid #B06AB3; + border-radius: 40rpx; + background: #FFFFFF; +} + +.qr-grid { + width: 300rpx; + height: 300rpx; + background: #F3F4F6; + border-radius: 24rpx; + display: grid; + grid-template-columns: repeat(8, 1fr); + gap: 8rpx; + padding: 24rpx; + position: relative; + overflow: hidden; +} + +.qr-dot { + width: 100%; + height: 100%; + border-radius: 4rpx; +} + +.qr-logo { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.qr-logo app-icon { + background: #FFFFFF; + border-radius: 16rpx; + padding: 12rpx; + box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1); +} + +.success-desc { + text-align: center; + color: #9CA3AF; + font-size: 28rpx; + line-height: 1.6; + margin-bottom: 48rpx; + display: flex; + flex-direction: column; +} + +.close-btn { + width: 100%; + background: #B06AB3; + padding: 28rpx 0; + border-radius: 24rpx; + font-size: 32rpx; + font-weight: 800; + color: #FFFFFF; + box-shadow: 0 8rpx 24rpx rgba(176, 106, 179, 0.2); +} + +/* ==================== GF100 弹窗样式 ==================== */ +.gf100-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.75); + display: flex; + align-items: center; + justify-content: center; + z-index: 2000; + backdrop-filter: blur(5px); +} + +.gf100-content { + position: relative; + width: 62.5%; + max-width: 480rpx; + animation: gf100In 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes gf100In { + from { + transform: scale(0.5); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +.gf100-image { + width: 100%; + display: block; + border-radius: 20rpx; + box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.5); +} + +.gf100-close { + position: absolute; + top: -80rpx; + right: 0; + width: 60rpx; + height: 60rpx; + border: 2rpx solid #fff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.close-icon { + color: #fff; + font-size: 40rpx; + line-height: 1; +} + diff --git a/pages/promote-poster/promote-poster.js b/pages/promote-poster/promote-poster.js new file mode 100644 index 0000000..1e53059 --- /dev/null +++ b/pages/promote-poster/promote-poster.js @@ -0,0 +1,247 @@ +const api = require('../../utils/api') +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + posters: [], + currentPosterIndex: 0, + qrCodeUrl: '', + referralCode: '', + canvasWidth: 1080, + canvasHeight: 1920, + isLoading: true, + userInfo: null + }, + + onLoad(options) { + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight, + userInfo: app.globalData.userInfo || wx.getStorageSync('user_info') + }) + + this.loadData(); + }, + + async loadData() { + try { + // 1. 获取推荐码 + const statsRes = await api.commission.getStats(); + let referralCode = 'default'; + if (statsRes.success && statsRes.data) { + referralCode = statsRes.data.referralCode; + this.setData({ referralCode }); + } + + // 2. 设置小程序二维码地址 + // scene 格式必须为 r=XXX 才能被 app.js 正确解析 + const baseUrl = app.globalData.baseUrl || 'https://ai-c.maimanji.com'; + const qrCodeUrl = `${baseUrl}/api/user/qrcode?scene=r=${referralCode}&page=pages/index/index`; + this.setData({ qrCodeUrl }); + + // 3. 获取动态海报背景列表 + const assetRes = await api.pageAssets.getAssets('posters'); + if (assetRes && assetRes.success && assetRes.data && assetRes.data.length > 0) { + const posters = assetRes.data.map(item => { + let url = (item.asset_url || '').trim(); + // 如果是相对路径,补充完整域名 + if (url && !url.startsWith('http')) { + url = baseUrl + (url.startsWith('/') ? '' : '/') + url; + } + return { + id: item.asset_key, + url: url, + qrBottom: 4.5, // 对应 1920 高度下的位置 + qrRight: 8 // 对应 1080 宽度下的位置 + }; + }); + this.setData({ posters, isLoading: false }); + } else { + // 兜底默认海报 + this.setData({ + posters: [ + { id: 'default', url: 'https://ai-c.maimanji.com/uploads/assets/poster-1.png', qrBottom: 4.5, qrRight: 8 } + ], + isLoading: false + }); + } + } catch (err) { + console.error('[promote-poster] loadData failed:', err); + this.setData({ isLoading: false }); + } + }, + + onPosterChange(e) { + this.setData({ currentPosterIndex: e.detail.current }) + }, + + onImageError(e) { + console.error('[promote-poster] 海报图加载失败:', e.detail.errMsg); + wx.showToast({ title: '海报背景加载失败', icon: 'none' }); + }, + + onQrError(e) { + console.error('[promote-poster] 二维码加载失败:', e.detail.errMsg); + wx.showToast({ title: '二维码加载失败', icon: 'none' }); + }, + + /** + * 下载文件辅助函数 + */ + downloadFile(url) { + return new Promise((resolve, reject) => { + wx.downloadFile({ + url, + success: res => { + if (res.statusCode === 200) resolve(res.tempFilePath); + else reject(new Error('Download failed: ' + url)); + }, + fail: err => { + console.error('Download failed:', url, err); + reject(err); + } + }); + }); + }, + + async savePoster() { + if (this.data.posters.length === 0) return; + + wx.showLoading({ title: '生成中...', mask: true }); + + try { + const template = this.data.posters[this.data.currentPosterIndex]; + + // 1. 下载资源 + const [bgPath, qrPath] = await Promise.all([ + this.downloadFile(template.url), + this.downloadFile(this.data.qrCodeUrl) + ]); + + // 2. 初始化 Canvas + const query = wx.createSelectorQuery() + query.select('#posterCanvas') + .fields({ node: true, size: true }) + .exec(async (res) => { + if (!res[0] || !res[0].node) { + wx.hideLoading(); + wx.showToast({ title: 'Canvas初始化失败', icon: 'none' }); + return; + } + + const canvas = res[0].node + const ctx = canvas.getContext('2d') + const dpr = wx.getSystemInfoSync().pixelRatio + + const canvasW = 1080; + const canvasH = 1920; + + canvas.width = canvasW * dpr + canvas.height = canvasH * dpr + ctx.scale(dpr, dpr) + + // 3. 绘制背景 + const bgImg = canvas.createImage(); + bgImg.src = bgPath; + await new Promise((resolve, reject) => { + bgImg.onload = resolve; + bgImg.onerror = reject; + }); + ctx.drawImage(bgImg, 0, 0, canvasW, canvasH); + + // 4. 绘制二维码 + const qrImg = canvas.createImage(); + qrImg.src = qrPath; + await new Promise((resolve, reject) => { + qrImg.onload = resolve; + qrImg.onerror = reject; + }); + + // 识别白框位置:根据 1080x1920 设计稿,白框在右下角 + // 二维码尺寸 260x260 + const qrW = 260; + const qrX = 720; // 1080 - 100 - 260 + const qrY = 1580; // 1920 - 80 - 260 + + // 绘制二维码背景(白框内可能需要微调,这里先绘制一个纯白背景确保清晰) + ctx.fillStyle = '#FFFFFF'; + ctx.beginPath(); + this.roundRect(ctx, qrX - 5, qrY - 5, qrW + 10, qrW + 10, 10); + ctx.fill(); + + ctx.drawImage(qrImg, qrX, qrY, qrW, qrW); + + // 6. 导出 + setTimeout(() => { + wx.canvasToTempFilePath({ + canvas: canvas, + success: (fileRes) => { + wx.saveImageToPhotosAlbum({ + filePath: fileRes.tempFilePath, + success: () => { + wx.hideLoading(); + wx.showToast({ title: '已保存到相册', icon: 'success' }) + }, + fail: (err) => { + wx.hideLoading(); + if (err.errMsg.indexOf('auth deny') !== -1) { + wx.showModal({ + title: '提示', + content: '请授权保存图片到相册', + success: (sm) => { + if (sm.confirm) wx.openSetting(); + } + }); + } else { + wx.showToast({ title: '保存失败', icon: 'none' }) + } + } + }) + }, + fail: (err) => { + console.error('canvasToTempFilePath fail:', err); + wx.hideLoading(); + wx.showToast({ title: '生成图片失败', icon: 'none' }); + } + }) + }, 300); + }) + } catch (err) { + console.error('[promote-poster] savePoster error:', err); + wx.hideLoading(); + wx.showToast({ title: '海报资源加载失败', icon: 'none' }); + } + }, + + roundRect(ctx, x, y, w, h, r) { + ctx.moveTo(x + r, y); + ctx.arcTo(x + w, y, x + w, y + h, r); + ctx.arcTo(x + w, y + h, x, y + h, r); + ctx.arcTo(x, y + h, x, y, r); + ctx.arcTo(x, y, x + w, y, r); + }, + + onShareAppMessage() { + const title = "发现一个超赞的AI情感陪伴官,快来看看吧!"; + const imageUrl = this.data.posters[this.data.currentPosterIndex]?.url || ''; + return { + title: title, + path: `/pages/index/index?referralCode=${this.data.referralCode}`, + imageUrl: imageUrl + } + }, + + goBack() { + wx.navigateBack(); + } +}); diff --git a/pages/promote-poster/promote-poster.json b/pages/promote-poster/promote-poster.json new file mode 100644 index 0000000..8835af0 --- /dev/null +++ b/pages/promote-poster/promote-poster.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/pages/promote-poster/promote-poster.wxml b/pages/promote-poster/promote-poster.wxml new file mode 100644 index 0000000..6c25ee8 --- /dev/null +++ b/pages/promote-poster/promote-poster.wxml @@ -0,0 +1,39 @@ + + + + + + 返回 + + 生成推广海报 + + + + + + + + + + + + + + + + + + + + + + + + + 正在加载素材... + + + + + + diff --git a/pages/promote-poster/promote-poster.wxss b/pages/promote-poster/promote-poster.wxss new file mode 100644 index 0000000..03f36ce --- /dev/null +++ b/pages/promote-poster/promote-poster.wxss @@ -0,0 +1,117 @@ +.page { + min-height: 100vh; + background-color: #F8F9FC; + display: flex; + flex-direction: column; +} + +.content { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding-bottom: 60rpx; +} + +.poster-swiper { + width: 100%; + height: 1000rpx; + margin-top: 40rpx; +} + +.poster-item { + display: flex; + justify-content: center; + align-items: center; +} + +.poster-card { + width: 540rpx; + height: 960rpx; + border-radius: 20rpx; + overflow: hidden; + position: relative; + transform: scale(0.9); + transition: all 0.3s ease; + box-shadow: 0 10rpx 30rpx rgba(0,0,0,0.1); +} + +.poster-card.active { + transform: scale(1); + box-shadow: 0 20rpx 40rpx rgba(176, 106, 179, 0.3); +} + +.poster-img { + width: 100%; + height: 100%; + background: #eee; +} + +.qr-overlay { + position: absolute; + width: 130rpx; + height: 130rpx; + background: #fff; + padding: 10rpx; + border-radius: 12rpx; +} + +.qr-img { + width: 100%; + height: 100%; +} + +.action-area { + margin-top: 60rpx; + width: 80%; + display: flex; + flex-direction: column; + gap: 24rpx; +} + +.save-btn { + background: linear-gradient(135deg, #CF91D3 0%, #B06AB3 100%); + color: white; + font-weight: 600; + border-radius: 50rpx; + width: 100%; + padding: 24rpx 0; + font-size: 30rpx; +} + +.share-btn { + background: white; + color: #B06AB3; + font-weight: 600; + border-radius: 50rpx; + width: 100%; + padding: 24rpx 0; + font-size: 30rpx; + border: 2rpx solid #B06AB3; +} + +.loading-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 100rpx 0; + color: #9ca3af; + font-size: 28rpx; +} + +.loading-spinner { + width: 60rpx; + height: 60rpx; + border: 6rpx solid #f3f3f3; + border-top: 6rpx solid #B06AB3; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 20rpx; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/pages/promote/promote.js b/pages/promote/promote.js new file mode 100644 index 0000000..a126e3a --- /dev/null +++ b/pages/promote/promote.js @@ -0,0 +1,386 @@ +// pages/promote/promote.js - 合作推广页面 + +const api = require('../../utils/api') + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + showModal: false, // 控制弹窗显示 + // 推广数据 + isDistributor: false, + referralCode: '', + commissionStats: { + totalReferrals: 0, + commissionBalance: 0, + totalEarned: 0 + }, + referrals: [], // Recent referrals for grid + defaultAvatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=500&auto=format&fit=crop&q=60', + loading: false, + // 分享配置 + shareConfig: null + }, + + onLoad() { + // Calculat nav bar height + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight + }) + + this.loadPromotionData() + this.loadShareConfig() + }, + + /** + * 加载分享配置 + */ + async loadShareConfig() { + try { + const res = await api.promotion.getShareConfig('promote') + if (res.success && res.data) { + const util = require('../../utils/util') + const shareConfig = { + ...res.data, + imageUrl: res.data.imageUrl ? util.getFullImageUrl(res.data.imageUrl) : '' + } + this.setData({ shareConfig }) + } + } catch (error) { + console.error('加载分享配置失败:', error) + } + }, + + /** + * 加载推广数据 + */ + async loadPromotionData() { + this.setData({ loading: true }) + + try { + // 1. Get Stats + const res = await api.commission.getStats() + if (res.success && res.data) { + this.setData({ + commissionStats: { + totalReferrals: res.data.totalReferrals || 0, + commissionBalance: (res.data.commissionBalance || 0).toFixed(2), + totalEarned: res.data.totalCommission || 0, + // 绑定到 WXML 中的字段 + shareCount: res.data.shareCount || 0, + registeredCount: res.data.totalReferrals || 0, + orderCount: res.data.orderCount || 0 + }, + referralCode: res.data.referralCode || '', + isDistributor: res.data.isDistributor || false + }) + } + + // 2. Get Referrals Preview (Limit 10 for list) + const refRes = await api.commission.getReferrals({ page: 1, limit: 10, sortBy: 'time' }) + if (refRes.success && refRes.data) { + const util = require('../../utils/util') + const list = (refRes.data.list || refRes.data || []).map(item => { + let avatar = item.avatar || item.avatarUrl || item.userAvatar; + if (avatar) { + avatar = util.getFullImageUrl(avatar); + } + const joinedAtRaw = item.boundAt || item.createdAt || ''; + const joinedAt = joinedAtRaw ? util.formatTime(joinedAtRaw, 'YYYY-MM-DD') : ''; + return { + userId: item.id || item.userId, + userName: item.nickname || item.userName || item.name || '未知用户', + userAvatar: avatar || this.data.defaultAvatar, + level: item.levelText || item.roleName || '普通用户', + joinedAt: joinedAt, + referralCount: item.referralCount || 0, + performance: item.totalContribution || 0 + }; + }); + this.setData({ + referrals: list + }); + } + + this.setData({ loading: false }) + } catch (error) { + console.error('加载推广数据失败:', error) + this.setData({ loading: false }) + } + }, + + onWithdraw() { + wx.navigateTo({ + url: '/pages/withdraw/withdraw' + }) + }, + + /** + * 跳转到全部推荐用户列表 + */ + goToReferrals() { + wx.navigateTo({ + url: '/pages/referrals/referrals' + }) + }, + + /** + * 返回上一页 + */ + goBack() { + wx.navigateBack({ + fail: () => { + // 如果无法返回,跳转到个人中心 + wx.switchTab({ url: '/pages/profile/profile' }) + } + }) + }, + + /** + * 显示合作推广弹窗 + */ + showPromotionModal() { + this.setData({ showModal: true }) + }, + + /** + * 关闭合作推广弹窗 + */ + closeModal() { + this.setData({ showModal: false }) + }, + + /** + * 阻止弹窗内容区域点击事件冒泡 + */ + preventClose() { + // 空函数,阻止事件冒泡 + }, + + /** + * 申请合作推广 + */ + applyPromotion() { + if (this.data.isDistributor) { + wx.showToast({ + title: '您已是分销商', + icon: 'none', + duration: 2000 + }) + return + } + + wx.showModal({ + title: '成为分销商', + content: '购买身份卡即可成为分销商,推荐好友赚佣金!', + confirmText: '了解详情', + cancelText: '暂不需要', + confirmColor: '#B06AB3', + success: (res) => { + if (res.confirm) { + // 跳转到充值页面购买身份卡 + wx.navigateTo({ + url: '/pages/recharge/recharge' + }) + } + } + }) + }, + + /** + * 跳转到佣金中心 + */ + goToCommission() { + wx.navigateTo({ + url: '/pages/commission/commission' + }) + }, + + goToPoster() { + wx.navigateTo({ + url: '/pages/promote-poster/promote-poster' + }) + }, + + /** + * 预览推广素材 + */ + previewMaterial(e) { + const { index } = e.currentTarget.dataset + const material = this.data.materials[index] + + wx.previewImage({ + current: material.image, + urls: this.data.materials.map(m => m.image) + }) + }, + + /** + * 下载推广素材 + */ + downloadMaterial(e) { + const { index } = e.currentTarget.dataset + const material = this.data.materials[index] + + wx.showLoading({ title: '下载中...' }) + + wx.downloadFile({ + url: material.image, + success: (res) => { + wx.hideLoading() + if (res.statusCode === 200) { + wx.saveImageToPhotosAlbum({ + filePath: res.tempFilePath, + success: () => { + wx.showToast({ + title: '已保存到相册', + icon: 'success' + }) + }, + fail: () => { + wx.showToast({ + title: '保存失败', + icon: 'none' + }) + } + }) + } + }, + fail: () => { + wx.hideLoading() + wx.showToast({ + title: '下载失败', + icon: 'none' + }) + } + }) + }, + + /** + * 查看教程详情 + */ + viewTutorial(e) { + const { index } = e.currentTarget.dataset + const tutorial = this.data.tutorials[index] + + wx.showModal({ + title: tutorial.title, + content: tutorial.content, + showCancel: false, + confirmText: '知道了' + }) + }, + + /** + * 复制推荐码 + */ + copyReferralCode() { + if (!this.data.referralCode) { + wx.showToast({ + title: '暂无推荐码', + icon: 'none' + }) + return + } + + wx.setClipboardData({ + data: this.data.referralCode, + success: () => { + wx.showToast({ + title: '已复制推荐码', + icon: 'success' + }) + } + }) + }, + + /** + * 分享推荐码 + */ + onShareAppMessage() { + const { referralCode, isDistributor, shareConfig } = this.data + const referralCodeParam = referralCode ? `?referralCode=${referralCode}` : '' + + // 记录分享行为 + api.promotion.recordShare({ + type: 'app_message', + page: '/pages/promote/promote', + referralCode: referralCode + }).then(() => { + this.loadPromotionData() + }).catch(err => console.error('记录分享失败:', err)) + + // 如果有分享配置且是分销商,使用配置内容 + if (shareConfig && isDistributor && referralCode) { + return { + title: shareConfig.title, + path: `${shareConfig.path || '/pages/index/index'}${referralCodeParam}`, + imageUrl: shareConfig.imageUrl + } + } + + // 默认分享逻辑 + if (!isDistributor || !referralCode) { + return { + title: '心伴AI - 情感陪伴聊天机器人', + path: '/pages/index/index', + imageUrl: '/images/share-cover.jpg' + } + } + + return { + title: `我的推荐码:${referralCode},注册即可享受优惠!`, + path: `/pages/index/index?referralCode=${referralCode}`, + imageUrl: '/images/share-commission.png' + } + }, + + /** + * 分享到朋友圈 + */ + onShareTimeline() { + const { referralCode, isDistributor, shareConfig } = this.data + const query = referralCode ? `referralCode=${referralCode}` : '' + + // 记录分享行为 + api.promotion.recordShare({ + type: 'timeline', + page: '/pages/promote/promote', + referralCode: referralCode + }).then(() => { + this.loadPromotionData() + }).catch(err => console.error('记录分享失败:', err)) + + // 如果有分享配置且是分销商,使用配置内容 + if (shareConfig && isDistributor && referralCode) { + return { + title: shareConfig.title, + query: query || shareConfig.query, + imageUrl: shareConfig.imageUrl + } + } + + // 默认分享逻辑 + if (!isDistributor || !referralCode) { + return { + title: '心伴AI - 情感陪伴聊天机器人', + imageUrl: '/images/share-cover.jpg' + } + } + + return { + title: `我的推荐码:${referralCode},注册即可享受优惠!`, + query: `referralCode=${referralCode}`, + imageUrl: '/images/share-commission.png' + } + } +}) diff --git a/pages/promote/promote.json b/pages/promote/promote.json new file mode 100644 index 0000000..780ca80 --- /dev/null +++ b/pages/promote/promote.json @@ -0,0 +1,7 @@ +{ + "navigationStyle": "custom", + "enablePullDownRefresh": true, + "usingComponents": { + "app-icon": "/components/icon/icon" + } +} diff --git a/pages/promote/promote.wxml b/pages/promote/promote.wxml new file mode 100644 index 0000000..4103867 --- /dev/null +++ b/pages/promote/promote.wxml @@ -0,0 +1,123 @@ + + + + + + + + + + + 推广中心 + + + + + + + {{commissionStats.shareCount}} + 分享次数 + + + {{commissionStats.registeredCount}} + 注册人数 + + + {{commissionStats.orderCount}} + 下单人数 + + + + + + + + + + + + 已推广用户 + + + 全部 + + + + + + + + 暂无推广用户 + + + + + + + + + 推荐 + {{item.referralCount}} + + + 业绩 + ¥{{item.performance}} + + + + + + + + + + + + + + 生成推广海报 + + + + + + + + + + 复制推广码 ({{referralCode || '...'}}) + + + + + + + + + + + diff --git a/pages/promote/promote.wxss b/pages/promote/promote.wxss new file mode 100644 index 0000000..9b555bf --- /dev/null +++ b/pages/promote/promote.wxss @@ -0,0 +1,338 @@ +/* pages/promote/promote.wxss */ +.page { + min-height: 100vh; + background-color: #F8F9FC; + display: flex; + flex-direction: column; +} + +/* Header Section (Purple Gradient) */ +.header-section { + background: linear-gradient(180deg, #D499D8 0%, #B06AB3 100%); + padding-bottom: 60rpx; /* Space for content overlap if needed, or just padding */ + position: relative; + border-radius: 0 0 40rpx 40rpx; +} + +/* Nav Bar */ +.nav-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + z-index: 100; +} + +.nav-bar { + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.nav-back { + position: absolute; + left: 10rpx; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + padding: 20rpx; + z-index: 10; +} + +.nav-title { + font-size: 34rpx; + font-weight: 700; + color: #FFFFFF; +} + +/* Stats Grid */ +.stats-grid { + display: flex; + justify-content: space-around; + padding: 40rpx 0; + margin-top: 20rpx; +} + +.stats-item { + display: flex; + flex-direction: column; + align-items: center; + color: #FFFFFF; +} + +.stats-value { + font-size: 60rpx; + font-weight: 800; + margin-bottom: 12rpx; +} + +.stats-label { + font-size: 30rpx; + opacity: 0.95; + font-weight: 500; +} + +/* Amount Card */ +.amount-card { + padding: 0 40rpx; + margin-top: 20rpx; + color: #FFFFFF; +} + +.amount-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16rpx; +} + +.amount-label { + font-size: 28rpx; + opacity: 0.9; +} + +.history-btn-wrap { + display: flex; + align-items: center; + gap: 4rpx; + background: rgba(255, 255, 255, 0.2); + padding: 6rpx 16rpx; + border-radius: 20rpx; +} + +.history-text { + font-size: 24rpx; +} + +.amount-value-row { + display: flex; + align-items: center; + justify-content: space-between; +} + +.amount-value { + font-size: 80rpx; + font-weight: 900; + line-height: 1; +} + +.withdraw-btn { + background: #FFFFFF; + color: #B06AB3; + font-size: 28rpx; + font-weight: 700; + padding: 16rpx 40rpx; + border-radius: 40rpx; + box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1); +} + +/* Main Body */ +.main-body { + padding: 0 32rpx; + margin-top: -30rpx; /* Slight overlap */ +} + +/* Referral Panel */ +.referral-panel { + background: #FFFFFF; + border-radius: 32rpx; + padding: 32rpx; + margin-bottom: 32rpx; + box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.02); +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 32rpx; + padding-top:42rpx; +} + +.header-left { + display: flex; + align-items: center; + gap: 12rpx; +} + +.purple-dot { + width: 10rpx; + height: 38rpx; + background: #B06AB3; + border-radius: 6rpx; +} + +.panel-title { + font-size: 38rpx; + font-weight: 800; + color: #111827; +} + +.referral-count { + font-size: 38rpx; + font-weight: 800; + color: #B06AB3; +} + +.see-all { + font-size: 32rpx; + color: #B06AB3; + font-weight: 700; +} + +.user-list { + display: flex; + flex-direction: column; + gap: 32rpx; +} + +.user-row { + display: flex; + align-items: center; + padding: 32rpx 0; + border-bottom: 2rpx solid #F3F4F6; +} + +.user-row:last-child { + border-bottom: none; +} + +.user-avatar { + width: 110rpx; + height: 110rpx; + border-radius: 50%; + background: #F3F4F6; + margin-right: 24rpx; + flex-shrink: 0; +} + +.user-info { + flex: 1; + min-width: 0; +} + +.user-main { + display: flex; + align-items: center; + gap: 16rpx; + margin-bottom: 8rpx; +} + +.user-name { + font-size: 36rpx; + font-weight: 700; + color: #1F2937; + max-width: 240rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.user-level { + font-size: 24rpx; + color: #B06AB3; + background: rgba(176, 106, 179, 0.1); + padding: 4rpx 16rpx; + border-radius: 20rpx; + font-weight: 600; +} + +.user-sub { + font-size: 28rpx; + color: #4B5563; +} + +.user-stats { + display: flex; + gap: 24rpx; + text-align: right; +} + +.stat-item { + display: flex; + flex-direction: column; + min-width: 100rpx; +} + +.stat-label { + font-size: 24rpx; + color: #9CA3AF; + margin-bottom: 6rpx; +} + +.stat-value { + font-size: 30rpx; + font-weight: 700; + color: #1F2937; +} + +.empty-state { + width: 100%; + text-align: center; + color: #9CA3AF; + font-size: 26rpx; + padding: 40rpx 0; +} + +/* Menu List */ +.menu-list { + display: flex; + flex-direction: column; + gap: 24rpx; +} + +.menu-item { + background: #FFFFFF; + border-radius: 24rpx; + padding: 40rpx 32rpx; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.01); +} + +.menu-left { + display: flex; + align-items: center; + gap: 32rpx; +} + +.icon-circle { + width: 88rpx; + height: 88rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.icon-circle.purple { background: rgba(176, 106, 179, 0.1); } +.icon-circle.pink { background: rgba(236, 72, 153, 0.1); } +.icon-circle.orange { background: rgba(245, 158, 11, 0.1); } + +.menu-text { + font-size: 34rpx; + font-weight: 700; + color: #1F2937; +} + +.btn-reset { + background: none; + border: none; + padding: 40rpx 32rpx; /* Match menu-item padding */ + margin: 0; + line-height: normal; + border-radius: 24rpx; /* Match menu-item border-radius */ + display: flex !important; /* Force flex for button */ + flex-direction: row !important; + align-items: center !important; + justify-content: space-between !important; + box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.01); + background-color: #FFFFFF; +} + +.btn-reset::after { + border: none; +} diff --git a/pages/recharge/recharge.js b/pages/recharge/recharge.js new file mode 100644 index 0000000..be45c6b --- /dev/null +++ b/pages/recharge/recharge.js @@ -0,0 +1,151 @@ +const { request } = require('../../utils_new/request'); +const { payProduct } = require('../../utils_new/payment'); +const api = require('../../utils/api'); + +Page({ + data: { + statusBarHeight: 20, + navBarHeight: 44, + totalNavHeight: 64, + products: [], + vipPackages: [], + modal: { + visible: false, + type: 'product', + id: 0, + orderType: '', + title: '', + price: 0, + benefits: [] + } + }, + + onLoad() { + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 20 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight: statusBarHeight + navBarHeight + }) + }, + + onShow() { + this.loadProducts() + }, + + async loadProducts() { + try { + const res = await api.payment.getPackages() + const body = res.data || {} + const list = Array.isArray(body) ? body : body?.data || [] + + if (!list.length) return + + const raw = list.map((p) => { + const attrs = p.attributes || {} + + const gradientStart = p.gradientStart || attrs.gradient_start || '#60A5FA' + const gradientEnd = p.gradientEnd || attrs.gradient_end || '#2563EB' + const tagColor = p.tagColor || attrs.tag_color || '#1E3A8A' + const tagText = p.tagText || attrs.tag_text || '' + const benefits = p.benefits || attrs.benefits || [] + + const iconMap = { + first: 'gift', + month: 'gift', + yearly: 'gift', + year: 'crown', + svip: 'diamond', + soulmate: 'heart-filled', + guardian: 'diamond', + companion: 'crown', + listener: 'gift' + } + + let vipType = attrs.type || p.vipType || '' + if (vipType === 'monthly') vipType = 'month' + if (vipType === 'yearly') vipType = 'year' + if (vipType === 'lifetime') vipType = 'svip' + + return { + id: Number(p.id), + orderType: 'vip', + vipType, + price: Number(p.price) || 0, + originalPrice: Number(p.originalPrice) || 0, + title: p.title, + subtitle: p.subtitle || attrs.subtitle || (benefits[0] || ''), + tagText: tagText, + gradient: `background: linear-gradient(180deg, ${gradientStart}, ${gradientEnd});`, + tagStyle: `color: ${tagColor}; background: rgba(255,255,255,0.25);`, + icon: iconMap[vipType] || 'crown', + iconGradient: `background: linear-gradient(180deg, ${gradientStart}, ${gradientEnd});`, + priceColor: `color: ${gradientEnd};`, + checkColor: tagColor || gradientEnd, + borderStyle: `border-color: rgba(0,0,0,0.06); background: rgba(255,255,255,0.9);`, + btnGradient: `background: linear-gradient(90deg, ${gradientStart}, ${gradientEnd});`, + benefits, + isRecommend: p.isRecommend || attrs.is_recommended || false + } + }) + + this.setData({ + products: raw, + vipPackages: raw + }) + } catch (err) { + console.error('Failed to load products:', err) + } + }, + + onBack() { + wx.navigateBack({ delta: 1 }) + }, + + openProductPay(e) { + const id = Number(e.currentTarget.dataset.id) + const product = this.data.products.find(x => Number(x.id) === id) + if (!product) return + + const pkg = this.data.vipPackages.find(x => Number(x.id) === id) + const benefits = pkg?.benefits || [] + + this.setData({ + modal: { + visible: true, + type: 'product', + id: Number(product.id), + orderType: product.orderType, + title: product.title, + price: product.price, + benefits + } + }) + }, + + closeModal() { + this.setData({ 'modal.visible': false }) + }, + + async confirmAction() { + const modal = this.data.modal + + wx.showLoading({ title: '发起支付...' }) + try { + await payProduct({ + productId: modal.id, + orderType: modal.orderType + }) + wx.hideLoading() + wx.showToast({ title: '购买成功', icon: 'success' }) + this.closeModal() + } catch (err) { + wx.hideLoading() + wx.showToast({ title: err.message || '支付失败', icon: 'none' }) + } + } +}) diff --git a/pages/recharge/recharge.json b/pages/recharge/recharge.json new file mode 100644 index 0000000..3153ca5 --- /dev/null +++ b/pages/recharge/recharge.json @@ -0,0 +1,5 @@ +{ + "usingComponents": { + "app-icon": "/components/icon/icon" + } +} diff --git a/pages/recharge/recharge.wxml b/pages/recharge/recharge.wxml new file mode 100644 index 0000000..c2980d1 --- /dev/null +++ b/pages/recharge/recharge.wxml @@ -0,0 +1,101 @@ + + + + + + 返回 + + 购买中心 + + + + + + + + {{item.tagText}} + + + ¥ + {{item.price}} + ¥{{item.originalPrice}} + + {{item.title}} + {{item.subtitle}} + + 立即购买 + + + + + + + 套餐内容 + + + + + 推荐 + + + + + + {{item.title}} + {{item.subtitle}} + + + ¥{{item.price}} + ¥{{item.originalPrice}} + + + + + + + {{benefit}} + + + + + + + + + + + + + + + + 确认订单 + 请确认您的购买信息 + + + + + + + + {{modal.title}} + 购买开通 + + + ¥{{modal.price}} + + + + + + + {{benefit}} + + + + + + + diff --git a/pages/recharge/recharge.wxss b/pages/recharge/recharge.wxss new file mode 100644 index 0000000..ec36a0d --- /dev/null +++ b/pages/recharge/recharge.wxss @@ -0,0 +1,525 @@ +.page { + min-height: 100vh; + background: #E8C3D4; + display: flex; + flex-direction: column; +} + +/* 顶部导航栏已移除,改用全局 unified-header */ + +/* Tabs */ +.tabs { + background: #ffffff; + display: flex; + justify-content: center; + gap: 80rpx; + padding-bottom: 20rpx; + border-bottom-left-radius: 32rpx; + border-bottom-right-radius: 32rpx; + position: relative; + z-index: 10; +} + +.tab-item { + position: relative; + font-size: 32rpx; + font-weight: 700; + color: #9CA3AF; + padding: 10rpx 0; +} + +.tab-item.active { + color: #111827; + font-weight: 900; + font-size: 34rpx; +} + +.tab-indicator { + position: absolute; + bottom: -10rpx; + left: 50%; + transform: translateX(-50%); + width: 40rpx; + height: 6rpx; + background: #B06AB3; + border-radius: 999rpx; +} + +/* Content */ +.content { + flex: 1; +} + +.tab-content { + padding-bottom: 80rpx; +} + +/* Special Offers Grid */ +.special-offers { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 24rpx; + margin-bottom: 40rpx; + padding: 0 24rpx; +} + +.offer-card { + width: 100%; + height: 412rpx; + border-radius: 32rpx; + padding: 32rpx; + box-sizing: border-box; + position: relative; + color: #ffffff; + display: flex; + flex-direction: column; + justify-content: space-between; + box-shadow: 0 10rpx 15rpx -3rpx rgba(0,0,0,0.1); + overflow: hidden; +} + +.offer-tag { + position: absolute; + top: 0; + right: 0; + padding: 10rpx 24rpx; + border-bottom-left-radius: 28rpx; + font-size: 24rpx; + font-weight: 900; + letter-spacing: 0.5px; +} + +.offer-body { + position: relative; + z-index: 2; + margin-top: 10rpx; +} + +.offer-price-row { + display: flex; + align-items: baseline; + margin-bottom: 8rpx; +} + +.offer-symbol { + font-size: 32rpx; + font-weight: 700; + margin-right: 4rpx; +} + +.offer-price { + font-size: 72rpx; + font-weight: 700; + line-height: 1; + letter-spacing: -2rpx; +} + +.offer-oprice { + font-size: 28rpx; + text-decoration: line-through; + opacity: 0.9; + margin-left: 12rpx; + font-weight: 700; +} + +.offer-title { + display: block; + font-size: 40rpx; + font-weight: 900; + margin-top: 12rpx; + line-height: 1.4; + letter-spacing: -1rpx; +} + +.offer-sub { + display: block; + font-size: 28rpx; + font-weight: 700; + opacity: 1; + margin-top: 4rpx; + letter-spacing: 0.5px; +} + +.offer-btn { + background: rgba(0,0,0,0.15); + text-align: center; + padding: 20rpx 0; + border-radius: 999rpx; + font-size: 32rpx; + font-weight: 900; + box-shadow: 0 2rpx 4rpx rgba(0,0,0,0.1); + position: relative; + z-index: 2; + letter-spacing: 1.5px; +} + +/* Decorative background icon simulation */ +.offer-card::after { + content: ''; + position: absolute; + bottom: -20rpx; + right: -20rpx; + width: 180rpx; + height: 180rpx; + background-repeat: no-repeat; + background-size: contain; + opacity: 0.12; + transform: rotate(-15deg); + pointer-events: none; +} + +.offer-card.first::after { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 12 20 22 4 22 4 12'/%3E%3Crect x='2' y='7' width='20' height='5'/%3E%3Cline x1='12' y1='22' x2='12' y2='7'/%3E%3Cpath d='M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z'/%3E%3Cpath d='M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z'/%3E%3C/svg%3E"); +} + +.offer-card.month::after { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2'/%3E%3C/svg%3E"); +} + +.offer-card.year::after { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 4l3 12h14l3-12-6 7-4-7-4 7-6-7z'/%3E%3Cpath d='M19 16l1 3H4l1-3'/%3E%3C/svg%3E"); +} + +.offer-card.svip::after { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 3h12l4 6-10 12L2 9z'/%3E%3Cpath d='M11 3l-4 6 5 11 5-11-4-6'/%3E%3Cpath d='M2 9h20'/%3E%3C/svg%3E"); +} + +.offer-card.soulmate::after { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z'/%3E%3C/svg%3E"); +} + +/* Member Center */ +.member-center { + background: #ffffff; + border-radius: 40rpx; + padding: 32rpx; + border: 2rpx solid #F3D1EA; +} + +.section-head { + display: flex; + align-items: center; + gap: 16rpx; + margin-bottom: 32rpx; +} + +.section-indicator { + width: 8rpx; + height: 32rpx; + background: #B06AB3; + border-radius: 999rpx; +} + +.section-indicator.yellow { + background: #FFD700; +} + +.section-title { + font-size: 36rpx; + font-weight: 900; + color: #111827; +} + +.vip-only-tag { + margin-left: 16rpx; + background: #FFF9E6; + color: #B8860B; + font-size: 20rpx; + font-weight: 800; + padding: 4rpx 12rpx; + border-radius: 999rpx; + border: 2rpx solid rgba(255, 215, 0, 0.3); +} + +.vip-list { + display: flex; + flex-direction: column; + gap: 24rpx; +} + +.vip-item { + background: #FDF4F9; + border-radius: 32rpx; + padding: 32rpx; + border: 4rpx solid transparent; + position: relative; + overflow: hidden; +} + +.vip-tag-corner { + position: absolute; + top: 0; + right: 0; + background: #FFD700; + color: #7C2D12; + font-size: 20rpx; + font-weight: 900; + padding: 8rpx 24rpx; + border-bottom-left-radius: 24rpx; +} + +.vip-main { + display: flex; + align-items: center; + gap: 24rpx; + margin-bottom: 24rpx; +} + +.vip-icon-box { + width: 96rpx; + height: 96rpx; + border-radius: 24rpx; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8rpx 16rpx rgba(0,0,0,0.1); +} + +.vip-info { + flex: 1; +} + +.vip-title { + display: block; + font-size: 34rpx; + font-weight: 900; + color: #111827; +} + +.vip-desc { + display: block; + font-size: 24rpx; + font-weight: 700; + color: #6B7280; + margin-top: 4rpx; +} + +.vip-price-col { + text-align: right; +} + +.vip-price { + display: block; + font-size: 44rpx; + font-weight: 900; +} + +.vip-oprice { + display: block; + font-size: 24rpx; + color: #9CA3AF; + text-decoration: line-through; +} + +.vip-benefits { + margin-bottom: 24rpx; + padding-left: 8rpx; + display: flex; + flex-direction: column; + gap: 12rpx; +} + +.benefit-row { + display: flex; + align-items: center; + gap: 12rpx; +} + +.benefit-text { + font-size: 26rpx; + color: #374151; + font-weight: 600; +} + +.vip-buy-btn { + width: 100%; + padding: 24rpx 0; + border-radius: 24rpx; + color: #ffffff; + font-size: 30rpx; + font-weight: 900; + box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.1); +} + +/* Shop Section */ +.shop-section { + /* same as member center logic */ +} + +.shop-grid { + display: flex; + flex-wrap: wrap; + gap: 24rpx; +} + +.shop-item { + width: calc(33.33% - 16rpx); + background: #ffffff; + border-radius: 24rpx; + overflow: hidden; + box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05); +} + +.shop-img-box { + height: 180rpx; + background: #F9FAFB; + position: relative; +} + +.shop-img { + width: 100%; + height: 100%; +} + +.exchange-badge { + position: absolute; + top: 8rpx; + right: 8rpx; + background: rgba(0,0,0,0.4); + backdrop-filter: blur(8rpx); + color: #ffffff; + font-size: 18rpx; + padding: 4rpx 12rpx; + border-radius: 999rpx; + font-weight: 700; +} + +.shop-info { + padding: 16rpx; +} + +.shop-name { + display: block; + font-size: 24rpx; + font-weight: 800; + color: #111827; + margin-bottom: 8rpx; +} + +.shop-cost-row { + display: flex; + align-items: center; + gap: 6rpx; +} + +.shop-cost { + font-size: 24rpx; + font-weight: 900; + color: #B06AB3; +} + +/* Modal */ +.modal { + position: fixed; + inset: 0; + z-index: 100; + display: flex; + align-items: center; + justify-content: center; +} + +.mask { + position: absolute; + inset: 0; + background: rgba(0,0,0,0.6); + backdrop-filter: blur(8rpx); +} + +.sheet { + position: relative; + width: 80%; + max-width: 600rpx; + background: #ffffff; + border-radius: 48rpx; + padding: 40rpx; + animation: popUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes popUp { + from { transform: scale(0.9); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} + +.sheet-head { + text-align: center; + margin-bottom: 40rpx; +} + +.sheet-title { + display: block; + font-size: 36rpx; + font-weight: 900; + color: #111827; +} + +.sheet-sub { + display: block; + font-size: 24rpx; + color: #6B7280; + margin-top: 8rpx; +} + +.sheet-card { + background: #F9FAFB; + border-radius: 32rpx; + padding: 32rpx; + display: flex; + align-items: center; + gap: 24rpx; + margin-bottom: 40rpx; +} + +.sheet-icon-box { + width: 96rpx; + height: 96rpx; + background: #ffffff; + border-radius: 24rpx; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05); +} + +.sheet-info { + flex: 1; +} + +.sheet-name { + display: block; + font-size: 32rpx; + font-weight: 900; + color: #111827; +} + +.sheet-desc { + font-size: 24rpx; + color: #6B7280; + margin-top: 4rpx; +} + +.sheet-price-box { + text-align: right; +} + +.sheet-price-val { + font-size: 40rpx; + font-weight: 900; + color: #B06AB3; +} + +.sheet-price-unit { + font-size: 20rpx; + color: #B06AB3; + margin-left: 4rpx; +} + +.sheet-btn { + width: 100%; + padding: 28rpx 0; + background: #B06AB3; + color: #ffffff; + border-radius: 24rpx; + font-size: 32rpx; + font-weight: 900; + box-shadow: 0 12rpx 24rpx rgba(176, 106, 179, 0.3); +} diff --git a/pages/referrals/orders/orders.js b/pages/referrals/orders/orders.js new file mode 100644 index 0000000..71cb5f0 --- /dev/null +++ b/pages/referrals/orders/orders.js @@ -0,0 +1,107 @@ +const api = require('../../../utils/api') +const util = require('../../../utils/util') + +Page({ + data: { + userInfo: null, + list: [], + page: 1, + pageSize: 20, + hasMore: true, + loading: false + }, + + onLoad(options) { + if (options.userInfo) { + try { + const userInfo = JSON.parse(decodeURIComponent(options.userInfo)) + this.setData({ userInfo }) + wx.setNavigationBarTitle({ + title: `${userInfo.name}的订单` + }) + } catch (e) { + console.error('解析用户信息失败', e) + } + } + + this.loadOrders() + }, + + onPullDownRefresh() { + this.setData({ + page: 1, + hasMore: true, + list: [] + }, () => { + this.loadOrders().then(() => { + wx.stopPullDownRefresh() + }) + }) + }, + + onReachBottom() { + if (this.data.loading || !this.data.hasMore) return + this.setData({ + page: this.data.page + 1 + }, () => { + this.loadOrders() + }) + }, + + async loadOrders() { + if (this.data.loading) return + + this.setData({ loading: true }) + + try { + const res = await api.commission.getRecords({ + page: this.data.page, + pageSize: this.data.pageSize, + fromUserId: this.data.userInfo?.userId + }) + + if (res.success) { + const newList = res.data.list.map(item => ({ + ...item, + orderTypeText: this.getOrderTypeText(item.orderType), + statusText: this.getStatusText(item.status), + statusClass: item.status, + createdAtFormatted: util.formatTime(new Date(item.createdAt)) + })) + + this.setData({ + list: this.data.page === 1 ? newList : [...this.data.list, ...newList], + hasMore: newList.length === this.data.pageSize + }) + } + } catch (err) { + console.error('加载订单失败', err) + wx.showToast({ + title: '加载失败', + icon: 'none' + }) + } finally { + this.setData({ loading: false }) + } + }, + + getOrderTypeText(type) { + const map = { + 'recharge': '充值', + 'vip': 'VIP会员', + 'identity_card': '身份卡', + 'agent_purchase': '智能体购买', + 'companion_chat': '陪聊' + } + return map[type] || type + }, + + getStatusText(status) { + const map = { + 'pending': '待结算', + 'settled': '已结算', + 'cancelled': '已取消' + } + return map[status] || status + } +}) diff --git a/pages/referrals/orders/orders.json b/pages/referrals/orders/orders.json new file mode 100644 index 0000000..4e9759b --- /dev/null +++ b/pages/referrals/orders/orders.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "用户订单", + "enablePullDownRefresh": true +} \ No newline at end of file diff --git a/pages/referrals/orders/orders.wxml b/pages/referrals/orders/orders.wxml new file mode 100644 index 0000000..db1a776 --- /dev/null +++ b/pages/referrals/orders/orders.wxml @@ -0,0 +1,61 @@ + + + + + + {{userInfo.name}} + 累计贡献:¥{{userInfo.contribution}} + + + + + + + + + {{item.orderTypeText}} + {{item.statusText}} + + + + 订单金额 + ¥{{item.orderAmount}} + + + 获得佣金 + +¥{{item.commissionAmount}} + + + 佣金比例 + {{item.commissionRate}}% + + + 时间 + {{item.createdAtFormatted}} + + + + + + + + + 该用户暂无贡献订单 + + + + + + 加载中... + + + + 没有更多了 + + + diff --git a/pages/referrals/orders/orders.wxss b/pages/referrals/orders/orders.wxss new file mode 100644 index 0000000..db683c0 --- /dev/null +++ b/pages/referrals/orders/orders.wxss @@ -0,0 +1,154 @@ +.container { + min-height: 100vh; + background-color: #f7f7f7; + display: flex; + flex-direction: column; +} + +.header { + background-color: #fff; + padding: 30rpx; + display: flex; + align-items: center; + margin-bottom: 20rpx; +} + +.avatar { + width: 100rpx; + height: 100rpx; + border-radius: 50%; + margin-right: 20rpx; + background-color: #eee; +} + +.info { + flex: 1; +} + +.name { + font-size: 32rpx; + font-weight: bold; + color: #333; + margin-bottom: 8rpx; +} + +.contribution { + font-size: 26rpx; + color: #666; +} + +.list-scroll { + flex: 1; + height: 0; +} + +.list-container { + padding: 0 20rpx; +} + +.list-item { + background-color: #fff; + border-radius: 12rpx; + margin-bottom: 20rpx; + padding: 30rpx; + box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05); +} + +.item-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 20rpx; + border-bottom: 1rpx solid #eee; + margin-bottom: 20rpx; +} + +.order-type { + font-size: 30rpx; + font-weight: bold; + color: #333; +} + +.status { + font-size: 26rpx; + padding: 4rpx 12rpx; + border-radius: 4rpx; +} + +.status.pending { + color: #faad14; + background-color: #fffbe6; + border: 1rpx solid #ffe58f; +} + +.status.settled { + color: #52c41a; + background-color: #f6ffed; + border: 1rpx solid #b7eb8f; +} + +.status.cancelled { + color: #999; + background-color: #f5f5f5; + border: 1rpx solid #d9d9d9; +} + +.item-content { + font-size: 28rpx; + color: #666; +} + +.row { + display: flex; + justify-content: space-between; + margin-bottom: 12rpx; +} + +.row:last-child { + margin-bottom: 0; +} + +.label { + color: #999; +} + +.value { + color: #333; +} + +.value.highlight { + color: #ff4d4f; + font-weight: bold; +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: 200rpx; + color: #999; + font-size: 28rpx; +} + +.empty-icon { + width: 200rpx; + height: 200rpx; + margin-bottom: 30rpx; +} + +.loading-state { + display: flex; + flex-direction: column; + align-items: center; + padding: 30rpx; + color: #999; + font-size: 24rpx; +} + +.no-more { + text-align: center; + padding: 30rpx; + color: #ccc; + font-size: 24rpx; +} diff --git a/pages/referrals/referrals.js b/pages/referrals/referrals.js new file mode 100644 index 0000000..1adbbb1 --- /dev/null +++ b/pages/referrals/referrals.js @@ -0,0 +1,265 @@ +// pages/referrals/referrals.js +const api = require('../../utils/api') +const util = require('../../utils/util') + +Page({ + data: { + // 导航栏高度 + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + + // 统计数据 + stats: { + totalReferrals: 0, + totalContribution: 0 + }, + + // 推荐用户列表 + list: [], + defaultAvatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=500&auto=format&fit=crop&q=60', + + // 分页 + page: 1, + pageSize: 20, + total: 0, + hasMore: true, + + // 状态 + loading: false, + isEmpty: false, + + // 搜索与筛选 + searchKeyword: '', + levelFilter: '', + levelRange: [ + { label: '全部等级', value: '' }, + { label: '心伴会员', value: 'soulmate' }, + { label: '守护会员', value: 'guardian' }, + { label: '陪伴会员', value: 'companion' }, + { label: '倾听会员', value: 'listener' }, + { label: '城市合伙人', value: 'partner' } + ], + levelIndex: 0 + }, + + onLoad(options) { + // 计算导航栏高度 + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight + }) + + // 加载数据 + this.loadReferrals() + }, + + /** + * 加载推荐用户列表 + */ + async loadReferrals(page = 1) { + if (this.data.loading) return + + this.setData({ loading: true }) + + try { + const res = await api.commission.getReferrals({ + page, + pageSize: this.data.pageSize, + keyword: this.data.searchKeyword, + level: this.data.levelFilter + }) + + console.log('推荐用户列表 API 响应:', res) + + // 处理不同的响应格式 + let listData = [] + let totalCount = 0 + + if (res.success && res.data) { + // 格式1: { success: true, data: { list: [...], total: 0 } } + listData = res.data.list || res.data || [] + totalCount = res.data.total || 0 + } else if (res.list) { + // 格式2: { list: [...], total: 0 } + listData = res.list || [] + totalCount = res.total || 0 + } else if (Array.isArray(res)) { + // 格式3: [...] + listData = res + totalCount = res.length + } else { + // 未知格式,设为空数组 + console.warn('未知的 API 响应格式:', res) + listData = [] + totalCount = 0 + } + + const roleMap = { + 'soulmate': '心伴会员', + 'guardian': '守护会员', + 'companion': '陪伴会员', + 'listener': '倾听会员', + 'partner': '城市合伙人' + }; + + // 预处理列表数据,增加等级显示 + const listDataProcessed = listData.map(item => { + const roleCode = item.distributorRole || item.role || ''; + + // 处理头像 + let avatar = item.userAvatar || item.avatar || item.avatarUrl; + if (avatar) { + avatar = util.getFullImageUrl(avatar); + } + + return { + ...item, + userAvatar: avatar || this.data.defaultAvatar, + levelText: roleMap[roleCode] || (item.isDistributor ? '分销会员' : '普通用户') + }; + }); + + const list = page === 1 ? listDataProcessed : [...this.data.list, ...listDataProcessed]; + const hasMore = listData.length === this.data.pageSize + const isEmpty = list.length === 0 + + // 计算统计数据 + const stats = { + totalReferrals: totalCount, + totalContribution: this.calculateTotalContribution(list) + } + + this.setData({ + stats, + list, + page, + total: totalCount, + hasMore, + isEmpty, + loading: false + }) + } catch (error) { + console.error('加载推荐用户列表失败:', error) + this.setData({ loading: false }) + wx.showToast({ + title: error.message || '加载失败', + icon: 'none' + }) + } + }, + + /** + * 计算累计贡献金额 + */ + calculateTotalContribution(list) { + return list.reduce((sum, item) => sum + (item.totalContribution || 0), 0) + }, + + /** + * 格式化金额 + */ + formatMoney(amount) { + return util.formatMoney(amount) + }, + + /** + * 格式化时间 + */ + formatTime(timestamp) { + return util.formatDate(timestamp) + }, + + /** + * 下拉刷新 + */ + onPullDownRefresh() { + this.loadReferrals(1).then(() => { + wx.stopPullDownRefresh() + }) + }, + + /** + * 上拉加载更多 + */ + onReachBottom() { + if (!this.data.hasMore || this.data.loading) return + this.loadReferrals(this.data.page + 1) + }, + + /** + * 返回上一页 + */ + onBack() { + wx.navigateBack() + }, + + /** + * 重新加载 + */ + onRetry() { + this.loadReferrals(1) + }, + + /** + * 搜索输入 + */ + onSearchInput(e) { + this.setData({ + searchKeyword: e.detail.value + }) + }, + + /** + * 确认搜索 + */ + onSearch() { + this.loadReferrals(1) + }, + + /** + * 清除搜索 + */ + onClearSearch() { + this.setData({ + searchKeyword: '' + }) + this.loadReferrals(1) + }, + + /** + * 等级筛选 + */ + onLevelChange(e) { + const index = e.detail.value + const level = this.data.levelRange[index].value + this.setData({ + levelIndex: index, + levelFilter: level + }) + this.loadReferrals(1) + }, + + /** + * 查看用户订单 + */ + viewOrders(e) { + const item = e.currentTarget.dataset.item + const userInfo = encodeURIComponent(JSON.stringify({ + userId: item.userId, + name: item.userName, + avatar: item.userAvatar, + contribution: item.totalContribution + })) + wx.navigateTo({ + url: `/pages/referrals/orders/orders?userInfo=${userInfo}` + }) + } +}) diff --git a/pages/referrals/referrals.json b/pages/referrals/referrals.json new file mode 100644 index 0000000..a6d230f --- /dev/null +++ b/pages/referrals/referrals.json @@ -0,0 +1,6 @@ +{ + "navigationStyle": "custom", + "enablePullDownRefresh": true, + "backgroundTextStyle": "dark", + "backgroundColor": "#F2EDFF" +} diff --git a/pages/referrals/referrals.wxml b/pages/referrals/referrals.wxml new file mode 100644 index 0000000..ed0b829 --- /dev/null +++ b/pages/referrals/referrals.wxml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + 推荐用户列表 + + + + + + + + + + + + + {{levelRange[levelIndex].label}} + + + + + + + + + + 总推荐人数 + {{stats.totalReferrals}}人 + + + + 累计贡献 + ¥{{formatMoney(stats.totalContribution)}} + + + + + + + + + + + + + + + 上拉加载更多 + + + + + 加载中... + + + + + 没有更多了 + + + + + + + 暂无推荐用户 + 分享您的推荐码,邀请好友注册 + + + + + + 加载中... + + + diff --git a/pages/referrals/referrals.wxss b/pages/referrals/referrals.wxss new file mode 100644 index 0000000..2fa88c3 --- /dev/null +++ b/pages/referrals/referrals.wxss @@ -0,0 +1,306 @@ +/* pages/referrals/referrals.wxss */ +.container { + min-height: 100vh; + background: #F2EDFF; +} + +/* 固定导航栏 */ +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; + background: rgba(242, 237, 255, 0.6); + backdrop-filter: blur(10px); +} + +.status-bar { + background: transparent; +} + +.nav-bar { + display: flex; + align-items: center; + justify-content: center; + padding: 0 32rpx; + background: transparent; + position: relative; +} + +.nav-back { + position: absolute; + left: 32rpx; + top: 50%; + transform: translateY(-50%); + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.back-icon { + width: 48rpx; + height: 48rpx; +} + +.nav-title { + font-size: 36rpx; + font-weight: 700; + color: #1F2937; + line-height: 1; +} + +/* 内容滚动区域 */ +.content-scroll { + height: 100vh; + box-sizing: border-box; +} + +/* 统计卡片 */ +.stats-card { + background: linear-gradient(135deg, #B06AB3 0%, #9B4D9E 100%); + border-radius: 24rpx; + padding: 48rpx 32rpx; + margin: 24rpx; + display: flex; + align-items: center; + justify-content: space-around; + box-shadow: 0 8rpx 24rpx rgba(176, 106, 179, 0.3); +} + +.stats-item { + flex: 1; + text-align: center; +} + +.stats-label { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.8); + margin-bottom: 16rpx; +} + +.stats-value { + font-size: 48rpx; + font-weight: 700; + color: #FFFFFF; +} + +.stats-divider { + width: 2rpx; + height: 80rpx; + background: rgba(255, 255, 255, 0.3); +} + +/* 列表容器 */ +.list-container { + padding: 0 24rpx 24rpx; +} + +/* 列表项 */ +.list-item { + background: #FFFFFF; + border-radius: 16rpx; + padding: 24rpx; + margin-bottom: 16rpx; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); +} + +.user-info { + display: flex; + align-items: center; + flex: 1; +} + +.user-avatar { + width: 96rpx; + height: 96rpx; + border-radius: 48rpx; + margin-right: 24rpx; + background: #F5F5F5; +} + +.user-details { + flex: 1; +} + +.user-name-row { + display: flex; + align-items: center; + gap: 12rpx; + margin-bottom: 8rpx; +} + +.user-name { + font-size: 36rpx; + font-weight: 600; + color: #1F2937; +} + +/* 筛选栏 */ +.filter-bar { + position: fixed; + left: 0; + right: 0; + height: 100rpx; + background: #FFFFFF; + display: flex; + align-items: center; + padding: 0 24rpx; + z-index: 98; + box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05); +} + +.search-box { + flex: 1; + height: 64rpx; + background: #F3F4F6; + border-radius: 32rpx; + display: flex; + align-items: center; + padding: 0 24rpx; + margin-right: 20rpx; +} + +.search-box input { + flex: 1; + font-size: 26rpx; + margin-left: 12rpx; + color: #1F2937; +} + +.level-picker { + display: flex; + align-items: center; + gap: 4rpx; + background: #F3F4F6; + padding: 0 20rpx; + height: 64rpx; + border-radius: 32rpx; +} + +.level-picker text { + font-size: 26rpx; + color: #4B5563; + max-width: 160rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.user-time { + font-size: 28rpx; + color: #9CA3AF; +} + +/* 列表项详情扩展 */ +.user-meta { + font-size: 26rpx; + color: #6B7280; + margin-top: 8rpx; + display: flex; + align-items: center; +} + +.user-level { + font-size: 24rpx; + color: #B06AB3; + background: rgba(176, 106, 179, 0.1); + padding: 4rpx 16rpx; + border-radius: 20rpx; + font-weight: 600; +} + +.user-contribution { + display: none; /* 已合并到详情中 */ +} + + +.arrow-right { + margin-left: 16rpx; + display: flex; + align-items: center; + flex-shrink: 0; +} + +.contribution-label { + font-size: 24rpx; + color: #9CA3AF; + margin-bottom: 8rpx; +} + +.contribution-value { + font-size: 32rpx; + font-weight: 700; + color: #B06AB3; +} + +/* 加载更多 */ +.load-more { + text-align: center; + padding: 32rpx 0; + font-size: 28rpx; + color: #9CA3AF; +} + +/* 空状态 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 120rpx 48rpx; +} + +.empty-icon { + width: 240rpx; + height: 240rpx; + margin-bottom: 32rpx; + opacity: 0.6; +} + +.empty-text { + font-size: 32rpx; + color: #6B7280; + margin-bottom: 16rpx; +} + +.empty-tip { + font-size: 28rpx; + color: #9CA3AF; +} + +/* 加载状态 */ +.loading-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 120rpx 48rpx; +} + +.loading-spinner { + width: 80rpx; + height: 80rpx; + border: 6rpx solid #E5E7EB; + border-top-color: #B06AB3; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.loading-text { + font-size: 28rpx; + color: #9CA3AF; + margin-top: 24rpx; +} diff --git a/pages/service-provider-detail/service-provider-detail.js b/pages/service-provider-detail/service-provider-detail.js new file mode 100644 index 0000000..85446b3 --- /dev/null +++ b/pages/service-provider-detail/service-provider-detail.js @@ -0,0 +1,318 @@ +// 服务人员详情页 +const api = require('../../utils/api') +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + provider: null, + reviews: [], + showBookingModal: false, + showAllReviewsModal: false, + // 预约表单数据 + selectedType: '', + serviceTypes: [], + bookingTime: '', + bookingAddress: '', + remark: '', + loading: true + }, + + onLoad(options) { + // 计算导航栏高度 + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight + }) + + // 加载服务人员数据 + if (options.id) { + this.loadProviderDetail(options.id) + } else { + // 使用模拟数据 + this.loadMockData() + } + }, + + /** + * 加载服务人员详情 + */ + async loadProviderDetail(id) { + this.setData({ loading: true }) + + try { + const res = await new Promise((resolve, reject) => { + wx.request({ + url: `${config.API_BASE_URL}/service/providers/${id}`, + method: 'GET', + success: (res) => resolve(res), + fail: (err) => reject(err) + }) + }) + + if (res.statusCode === 200 && res.data.success) { + const provider = res.data.data + const baseUrl = 'https://ai-c.maimanji.com' + + // 处理头像URL + if (provider.avatar && provider.avatar.startsWith('/')) { + provider.avatar = baseUrl + provider.avatar + } + + // 处理服务类型 + let serviceTypes = provider.serviceTypes || [] + if (serviceTypes.length === 0) { + // 默认服务类型 + serviceTypes = [ + { id: 'basic', name: '基础服务', price: provider.price, unit: provider.unit || '小时' } + ] + } + + // 模拟评价数据(暂时保留,因为后端未返回) + const reviews = [ + { + id: 1, + userName: '138****6172', + avatar: '', + rating: 5, + content: '非常专业,服务态度很好。工作认真负责,每次都能把家里打扫得干干净净。非常满意!', + tags: ['专业', '认真', '细心'], + date: '2024-01-15' + } + ] + + this.setData({ + provider, + reviews, + serviceTypes, + selectedType: serviceTypes[0].id, + loading: false + }) + } else { + wx.showToast({ title: '加载失败', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 2000) + } + } catch (err) { + console.error('加载服务人员详情失败', err) + wx.showToast({ title: '网络错误', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 2000) + } + }, + + /** + * 返回 + */ + onBack() { + wx.navigateBack() + }, + + /** + * 分享 + */ + onShare() { + wx.showShareMenu({ + withShareTicket: true, + menus: ['shareAppMessage', 'shareTimeline'] + }) + }, + + /** + * 咨询 + */ + onConsult() { + // 检查登录 + if (app.checkNeedLogin && app.checkNeedLogin()) return + + wx.showToast({ title: '咨询功能开发中', icon: 'none' }) + }, + + /** + * 立即预约 + */ + onBook() { + // 检查登录 + if (app.checkNeedLogin && app.checkNeedLogin()) return + + this.setData({ showBookingModal: true }) + }, + + /** + * 关闭预约弹窗 + */ + closeBookingModal() { + this.setData({ showBookingModal: false }) + }, + + /** + * 选择服务类型 + */ + selectServiceType(e) { + const id = e.currentTarget.dataset.id + this.setData({ selectedType: id }) + }, + + /** + * 选择预约时间 + */ + onPickTime() { + const now = new Date() + const currentDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}` + const currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}` + + wx.showModal({ + title: '选择时间', + content: '请使用日期时间选择器', + showCancel: false, + success: () => { + // 简化版:直接设置当前时间 + this.setData({ + bookingTime: `${currentDate} ${currentTime}` + }) + } + }) + }, + + /** + * 选择服务地址 + */ + onPickAddress() { + wx.chooseLocation({ + success: (res) => { + this.setData({ + bookingAddress: res.address + res.name + }) + }, + fail: (err) => { + if (err.errMsg.includes('auth deny')) { + wx.showModal({ + title: '需要位置权限', + content: '请在设置中开启位置权限', + confirmText: '去设置', + success: (res) => { + if (res.confirm) { + wx.openSetting() + } + } + }) + } + } + }) + }, + + /** + * 备注输入 + */ + onRemarkInput(e) { + this.setData({ remark: e.detail.value }) + }, + + /** + * 计算价格 + */ + calculatePrice() { + const { serviceTypes, selectedType } = this.data + const type = serviceTypes.find(t => t.id === selectedType) + return type ? type.price : 0 + }, + + /** + * 确认预约 + */ + async confirmBooking() { + const { selectedType, bookingTime, bookingAddress, remark, provider } = this.data + + // 验证表单 + if (!bookingTime) { + wx.showToast({ title: '请选择预约时间', icon: 'none' }) + return + } + + if (!bookingAddress) { + wx.showToast({ title: '请选择服务地址', icon: 'none' }) + return + } + + wx.showLoading({ title: '提交中...' }) + + try { + // TODO: 调用实际API + // const res = await api.service.createBooking({ + // providerId: provider.id, + // serviceType: selectedType, + // bookingTime, + // address: bookingAddress, + // remark + // }) + + // 模拟API调用 + await new Promise(resolve => setTimeout(resolve, 1000)) + + wx.hideLoading() + wx.showToast({ title: '预约成功', icon: 'success' }) + + this.setData({ showBookingModal: false }) + + // 跳转到订单详情 + setTimeout(() => { + wx.navigateTo({ + url: '/pages/order-detail/order-detail?id=mock123' + }) + }, 1500) + } catch (err) { + wx.hideLoading() + console.error('预约失败', err) + wx.showToast({ title: '预约失败', icon: 'none' }) + } + }, + + /** + * 查看全部评价 + */ + onViewAllReviews() { + this.setData({ showAllReviewsModal: true }) + }, + + /** + * 关闭全部评价弹窗 + */ + closeAllReviewsModal() { + this.setData({ showAllReviewsModal: false }) + }, + + /** + * 分享给好友 + */ + onShareAppMessage() { + const { provider } = this.data + const referralCode = wx.getStorageSync('referralCode') || '' + const referralCodeParam = referralCode ? `&referralCode=${referralCode}` : '' + return { + title: `推荐服务人员:${provider.name}`, + path: `/pages/service-provider-detail/service-provider-detail?id=${provider.id}${referralCodeParam}`, + imageUrl: provider.avatar + } + }, + + /** + * 分享到朋友圈 + */ + onShareTimeline() { + const { provider } = this.data + const referralCode = wx.getStorageSync('referralCode') || '' + const query = `id=${provider.id}${referralCode ? `&referralCode=${referralCode}` : ''}` + return { + title: `推荐服务人员:${provider.name}`, + query: query, + imageUrl: provider.avatar + } + } +}) diff --git a/pages/service-provider-detail/service-provider-detail.json b/pages/service-provider-detail/service-provider-detail.json new file mode 100644 index 0000000..b16ef3e --- /dev/null +++ b/pages/service-provider-detail/service-provider-detail.json @@ -0,0 +1,4 @@ +{ + "navigationStyle": "custom", + "usingComponents": {} +} diff --git a/pages/service-provider-detail/service-provider-detail.wxml b/pages/service-provider-detail/service-provider-detail.wxml new file mode 100644 index 0000000..0ed63e2 --- /dev/null +++ b/pages/service-provider-detail/service-provider-detail.wxml @@ -0,0 +1,308 @@ + + + + + + + + + + 服务详情 + + + + + + + + + + + + + + + + + + + + + 实名认证 + + + + + + + {{provider.name}} + {{provider.tag}} + + + + + + {{provider.experience}} + + + + {{provider.rating}} + + + + {{provider.desc}} + + + + + + + {{provider.hours}} + 服务时长 + + + + {{provider.serviceCount}} + 服务人次 + + + + {{provider.repeatRate}}% + 回头率 + + + + + + + + + 服务技能 + + + + + {{item}} + + + + + + + + + 服务介绍 + + {{provider.introduction}} + + + + + + + 服务案例 + + + + + {{item.title}} + + + + + + + + + + 用户评价 + ({{reviews.length}}) + + + 查看全部 + + + + + + + + + + {{item.date}} + + {{item.content}} + + + {{tag}} + + + + + + + + + + + + + + + ¥ + {{provider.price}} + {{provider.unit}} + + 起步价 + + + + + + 咨询 + + + 立即预约 + + + + + + + + + + 预约服务 + + × + + + + + + + + + {{provider.name}} + {{provider.tag}} + + + + + + 服务类型 + + + {{item.name}} + ¥{{item.price}}/{{item.unit}} + + + + + + + 预约时间 + + {{bookingTime || '请选择时间'}} + + + + + + + 服务地址 + + {{bookingAddress || '请选择地址'}} + + + + + + + 备注说明 + + {{remark.length}}/200 + + + + + + 服务费用 + ¥{{calculatePrice()}} + + + 平台服务费 + ¥0 + + + + 合计 + ¥{{calculatePrice()}} + + + + + + + + + + + + + + 全部评价 + + × + + + + + + + {{provider.rating}} + + + + {{reviews.length}}条评价 + + + {{provider.goodRate}}% + 好评率 + + + + + + + + + {{item.date}} + + {{item.content}} + + + {{tag}} + + + + + diff --git a/pages/service-provider-detail/service-provider-detail.wxss b/pages/service-provider-detail/service-provider-detail.wxss new file mode 100644 index 0000000..8abf09b --- /dev/null +++ b/pages/service-provider-detail/service-provider-detail.wxss @@ -0,0 +1,920 @@ +/* 服务人员详情页样式 */ +page { + background: #F9FAFB; + height: 100%; +} + +.page-container { + height: 100vh; + display: flex; + flex-direction: column; +} + +/* ========== 固定导航栏 ========== */ +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); +} + +.status-bar { + background: transparent; +} + +.nav-bar { + display: flex; + align-items: center; + justify-content: center; + padding: 0 32rpx; + background: transparent; + position: relative; +} + +.nav-back { + position: absolute; + left: 32rpx; + top: 50%; + transform: translateY(-50%); + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.back-icon { + width: 48rpx; + height: 48rpx; +} + +.nav-title { + font-size: 36rpx; + font-weight: 700; + color: #1F2937; + line-height: 1; +} + +.nav-share { + position: absolute; + right: 32rpx; + top: 50%; + transform: translateY(-50%); + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.share-icon { + width: 48rpx; + height: 48rpx; +} + +/* ========== 内容滚动区域 ========== */ +.content-scroll { + height: 100vh; + box-sizing: border-box; + padding-bottom: 240rpx; +} + +/* ========== 服务人员信息卡片 ========== */ +.provider-card { + background: #fff; + margin: 32rpx; + border-radius: 32rpx; + overflow: hidden; + box-shadow: 0 8rpx 24rpx -8rpx rgba(0, 0, 0, 0.08), 0 20rpx 30rpx -6rpx rgba(0, 0, 0, 0.06); + position: relative; +} + +.card-header-bg { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 280rpx; + background: linear-gradient(135deg, #FFF7ED 0%, #FEF3C7 50%, #FEE2E2 100%); + opacity: 0.6; +} + +.provider-info { + position: relative; + padding: 40rpx 32rpx 32rpx; + display: flex; + gap: 32rpx; +} + +.avatar-section { + flex-shrink: 0; +} + +.avatar-wrap { + width: 168rpx; + height: 168rpx; + position: relative; +} + +.avatar-img { + width: 168rpx; + height: 168rpx; + border-radius: 32rpx; + border: 4rpx solid #fff; + box-shadow: 0 8rpx 16rpx -4rpx rgba(0, 0, 0, 0.1); +} + +.verified-badge { + position: absolute; + bottom: -16rpx; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: 8rpx; + padding: 6rpx 20rpx; + background: linear-gradient(135deg, #FFFBEB 0%, #FEF3C7 100%); + border: 2rpx solid #FCD34D; + border-radius: 100rpx; + box-shadow: 0 4rpx 8rpx -2rpx rgba(0, 0, 0, 0.1); +} + +.verified-icon { + width: 24rpx; + height: 24rpx; +} + +.verified-text { + font-size: 20rpx; + font-weight: 700; + color: #B45309; +} + +.info-section { + flex: 1; + display: flex; + flex-direction: column; + gap: 16rpx; + padding-top: 8rpx; +} + +.name-row { + display: flex; + align-items: center; + gap: 16rpx; +} + +.provider-name { + font-size: 44rpx; + font-weight: 900; + color: #101828; + line-height: 1.2; +} + +.provider-tag { + padding: 6rpx 16rpx; + background: linear-gradient(135deg, #914584 0%, #B066A3 100%); + border-radius: 100rpx; + font-size: 22rpx; + font-weight: 700; + color: #fff; +} + +.experience-row { + display: flex; + align-items: center; + gap: 24rpx; +} + +.experience-item { + display: flex; + align-items: center; + gap: 8rpx; +} + +.exp-icon { + width: 28rpx; + height: 28rpx; +} + +.exp-text { + font-size: 28rpx; + font-weight: 600; + color: #4A5565; +} + +.rating-item { + display: flex; + align-items: center; + gap: 8rpx; +} + +.star-icon { + width: 28rpx; + height: 28rpx; +} + +.rating-text { + font-size: 28rpx; + font-weight: 700; + color: #F59E0B; +} + +.provider-desc { + font-size: 28rpx; + color: #6A7282; + line-height: 1.5; +} + +/* 统计数据 */ +.stats-section { + display: flex; + justify-content: space-around; + padding: 32rpx; + border-top: 2rpx solid #F3F4F6; + background: linear-gradient(180deg, rgba(255, 247, 237, 0.3) 0%, rgba(254, 243, 199, 0.3) 100%); +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 8rpx; +} + +.stat-value { + font-size: 48rpx; + font-weight: 900; + color: #101828; + line-height: 1; +} + +.stat-label { + font-size: 24rpx; + color: #6A7282; +} + +.stat-divider { + width: 2rpx; + height: 80rpx; + background: #E5E7EB; +} + +/* ========== 通用卡片样式 ========== */ +.skills-card, +.intro-card, +.cases-card, +.reviews-card { + background: #fff; + margin: 0 32rpx 32rpx; + padding: 32rpx; + border-radius: 24rpx; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); +} + +.card-title { + display: flex; + align-items: center; + gap: 16rpx; + margin-bottom: 24rpx; +} + +.title-dot { + width: 8rpx; + height: 8rpx; + background: #914584; + border-radius: 50%; +} + +.title-text { + font-size: 36rpx; + font-weight: 900; + color: #101828; +} + +.card-title-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24rpx; +} + +.review-count { + font-size: 28rpx; + color: #99A1AF; + margin-left: 8rpx; +} + +.view-all { + display: flex; + align-items: center; + gap: 8rpx; +} + +.view-all-text { + font-size: 28rpx; + color: #914584; +} + +.arrow-icon { + width: 32rpx; + height: 32rpx; +} + +/* ========== 服务技能 ========== */ +.skills-list { + display: flex; + flex-wrap: wrap; + gap: 20rpx; +} + +.skill-tag { + display: flex; + align-items: center; + gap: 12rpx; + padding: 16rpx 24rpx; + background: linear-gradient(135deg, #F0FDF4 0%, #DCFCE7 100%); + border: 2rpx solid #86EFAC; + border-radius: 100rpx; +} + +.check-icon { + width: 28rpx; + height: 28rpx; +} + +.skill-text { + font-size: 28rpx; + font-weight: 600; + color: #166534; +} + +/* ========== 服务介绍 ========== */ +.intro-content { + font-size: 30rpx; + color: #4A5565; + line-height: 1.8; +} + +/* ========== 服务案例 ========== */ +.cases-list { + display: flex; + gap: 24rpx; + overflow-x: auto; +} + +.case-item { + flex-shrink: 0; + width: 280rpx; +} + +.case-image { + width: 280rpx; + height: 280rpx; + border-radius: 16rpx; + margin-bottom: 16rpx; +} + +.case-title { + font-size: 28rpx; + color: #364153; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ========== 用户评价 ========== */ +.review-list { + display: flex; + flex-direction: column; + gap: 32rpx; +} + +.review-item { + padding-bottom: 32rpx; + border-bottom: 2rpx solid #F3F4F6; +} + +.review-item:last-child { + border-bottom: none; + padding-bottom: 0; +} + +.review-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 16rpx; +} + +.user-info { + display: flex; + gap: 16rpx; +} + +.user-avatar { + width: 72rpx; + height: 72rpx; + border-radius: 50%; +} + +.user-detail { + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.user-name { + font-size: 28rpx; + font-weight: 600; + color: #364153; +} + +.review-stars { + display: flex; + gap: 4rpx; +} + +.star-small { + width: 24rpx; + height: 24rpx; +} + +.review-date { + font-size: 24rpx; + color: #99A1AF; +} + +.review-content { + font-size: 28rpx; + color: #4A5565; + line-height: 1.6; + margin-bottom: 16rpx; +} + +.review-tags { + display: flex; + flex-wrap: wrap; + gap: 16rpx; +} + +.review-tag { + padding: 8rpx 20rpx; + background: #FFF7ED; + border-radius: 100rpx; +} + +.tag-text { + font-size: 24rpx; + color: #EA580C; +} + +/* ========== 底部占位 ========== */ +.bottom-placeholder { + height: 80rpx; +} + +/* ========== 底部操作栏 ========== */ +.bottom-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: #fff; + padding: 24rpx 32rpx; + padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); + border-top: 2rpx solid #F3F4F6; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04); + z-index: 100; +} + +.price-section { + display: flex; + flex-direction: column; + gap: 4rpx; +} + +.price-row { + display: flex; + align-items: baseline; +} + +.price-symbol { + font-size: 28rpx; + font-weight: 700; + color: #FF6B00; +} + +.price-value { + font-size: 56rpx; + font-weight: 900; + color: #FF6B00; + line-height: 1; +} + +.price-unit { + font-size: 24rpx; + color: #6A7282; + margin-left: 8rpx; +} + +.price-tip { + font-size: 24rpx; + color: #99A1AF; +} + +.action-buttons { + display: flex; + gap: 16rpx; +} + +.consult-btn { + width: 120rpx; + height: 88rpx; + background: #F3F4F6; + border-radius: 100rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4rpx; +} + +.btn-icon { + width: 36rpx; + height: 36rpx; +} + +.consult-btn .btn-text { + font-size: 24rpx; + color: #4A5565; +} + +.book-btn { + width: 240rpx; + height: 88rpx; + background: linear-gradient(135deg, #FF6B6B 0%, #FF8787 100%); + border-radius: 100rpx; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8rpx 20rpx -6rpx rgba(255, 107, 107, 0.4); +} + +.book-btn .btn-text { + font-size: 32rpx; + font-weight: 700; + color: #fff; +} + +/* ========== 预约弹窗 ========== */ +.booking-modal-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; +} + +.booking-modal { + position: fixed; + left: 0; + right: 0; + bottom: 0; + background: #fff; + border-radius: 32rpx 32rpx 0 0; + z-index: 1001; + max-height: 85vh; + display: flex; + flex-direction: column; + transform: translateY(100%); + transition: transform 0.3s ease; +} + +.booking-modal.show { + transform: translateY(0); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 32rpx; + border-bottom: 2rpx solid #F3F4F6; +} + +.modal-title { + font-size: 40rpx; + font-weight: 900; + color: #101828; +} + +.modal-close { + width: 64rpx; + height: 64rpx; + display: flex; + align-items: center; + justify-content: center; + background: #F3F4F6; + border-radius: 50%; +} + +.close-icon { + font-size: 48rpx; + color: #6A7282; + line-height: 1; +} + +.modal-content { + flex: 1; + padding: 32rpx; + overflow-y: auto; +} + +/* 预约表单 */ +.booking-provider { + display: flex; + align-items: center; + gap: 24rpx; + padding: 24rpx; + background: #F9FAFB; + border-radius: 16rpx; + margin-bottom: 32rpx; +} + +.provider-avatar-small { + width: 96rpx; + height: 96rpx; + border-radius: 16rpx; +} + +.provider-info-small { + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.provider-name-small { + font-size: 32rpx; + font-weight: 700; + color: #101828; +} + +.provider-tag-small { + font-size: 24rpx; + color: #6A7282; +} + +.form-section { + margin-bottom: 32rpx; +} + +.form-label { + display: block; + font-size: 28rpx; + font-weight: 600; + color: #364153; + margin-bottom: 16rpx; +} + +.service-type-list { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.type-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 24rpx; + background: #F9FAFB; + border: 2rpx solid #E5E7EB; + border-radius: 16rpx; + transition: all 0.2s; +} + +.type-item.active { + background: #FFF7ED; + border-color: #FF6B00; +} + +.type-name { + font-size: 30rpx; + font-weight: 600; + color: #364153; +} + +.type-price { + font-size: 28rpx; + font-weight: 700; + color: #FF6B00; +} + +.time-picker, +.address-picker { + display: flex; + justify-content: space-between; + align-items: center; + padding: 24rpx; + background: #F9FAFB; + border: 2rpx solid #E5E7EB; + border-radius: 16rpx; +} + +.time-text, +.address-text { + font-size: 30rpx; + color: #364153; +} + +.calendar-icon, +.location-icon { + width: 40rpx; + height: 40rpx; +} + +.remark-input { + width: 100%; + min-height: 160rpx; + padding: 24rpx; + background: #F9FAFB; + border: 2rpx solid #E5E7EB; + border-radius: 16rpx; + font-size: 28rpx; + color: #364153; + box-sizing: border-box; +} + +.char-count { + display: block; + text-align: right; + font-size: 24rpx; + color: #99A1AF; + margin-top: 8rpx; +} + +.price-summary { + padding: 24rpx; + background: #F9FAFB; + border-radius: 16rpx; + margin-top: 32rpx; +} + +.summary-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16rpx; +} + +.summary-row:last-child { + margin-bottom: 0; +} + +.summary-label { + font-size: 28rpx; + color: #6A7282; +} + +.summary-value { + font-size: 32rpx; + font-weight: 700; + color: #364153; +} + +.summary-row.total .summary-label { + font-size: 32rpx; + font-weight: 700; + color: #101828; +} + +.summary-row.total .summary-value { + font-size: 40rpx; + color: #FF6B00; +} + +.summary-divider { + height: 2rpx; + background: #E5E7EB; + margin: 16rpx 0; +} + +.modal-footer { + padding: 24rpx 32rpx; + padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); + border-top: 2rpx solid #F3F4F6; +} + +.confirm-btn { + width: 100%; + height: 96rpx; + background: linear-gradient(135deg, #FF6B6B 0%, #FF8787 100%); + border-radius: 100rpx; + font-size: 32rpx; + font-weight: 700; + color: #fff; + border: none; + box-shadow: 0 8rpx 20rpx -6rpx rgba(255, 107, 107, 0.4); +} + +.confirm-btn::after { + border: none; +} + +/* ========== 全部评价弹窗 ========== */ +.all-reviews-modal-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; +} + +.all-reviews-modal { + position: fixed; + left: 0; + right: 0; + bottom: 0; + background: #fff; + border-radius: 32rpx 32rpx 0 0; + z-index: 1001; + max-height: 85vh; + display: flex; + flex-direction: column; + transform: translateY(100%); + transition: transform 0.3s ease; +} + +.all-reviews-modal.show { + transform: translateY(0); +} + +.review-summary { + display: flex; + justify-content: space-between; + align-items: center; + padding: 32rpx; + border-bottom: 2rpx solid #F3F4F6; +} + +.summary-left { + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.summary-score { + font-size: 72rpx; + font-weight: 900; + color: #101828; + line-height: 1; +} + +.summary-stars { + display: flex; + gap: 8rpx; +} + +.summary-count { + font-size: 24rpx; + color: #99A1AF; +} + +.summary-right { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4rpx; +} + +.good-rate { + font-size: 56rpx; + font-weight: 900; + color: #FF6B00; + line-height: 1; +} + +.good-rate-label { + font-size: 24rpx; + color: #99A1AF; +} + +.all-reviews-list { + flex: 1; + padding: 32rpx; + overflow-y: auto; +} diff --git a/pages/service/service.js b/pages/service/service.js new file mode 100644 index 0000000..6354431 --- /dev/null +++ b/pages/service/service.js @@ -0,0 +1,293 @@ +// 服务页面 - 按照Figma设计实现 +const api = require('../../utils/api') +const config = require('../../config/index') + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + searchText: '', + hasNotification: true, + totalUnread: 0, + + // 轮播图 - 从后台素材管理API加载 + banners: [], + swiperHeight: 400, + + // 服务类型 - 6个 + serviceTypes: [ + { id: 'points', name: '礼品商城', icon: '/images/fuw-shangcheng.png', bgColor: '#FFF7ED' }, + { id: 'merchants', name: '合作商家', icon: '/images/fuw-shangjia.png', bgColor: '#FFF1F2' }, + { id: 'eldercare', name: '智慧康养', icon: '/images/fuw-kangyang.png', bgColor: '#F0FDFA' }, + { id: 'custom', name: '定制服务', icon: '/images/fuw-dingzhi.png', bgColor: '#F0F9FF' }, + { id: 'academy', name: '心伴学堂', icon: '/images/fuw-pinpai.png', bgColor: '#F5F3FF' }, + { id: 'brand', name: '关于品牌', icon: '/images/fuw-aixin.png', bgColor: '#FFF1F2' } + ], + + // 内容列表 (通用) + listData: [], + isLoading: false, + page: 1, + hasMore: true, + + // 审核状态 + auditStatus: 0 + }, + + onLoad() { + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight + }) + + // 加载Banner配置 + this.loadBanners() + + // 加载合作商家列表 + this.loadListData() + }, + + /** + * 轮播图图片加载完成,自适应高度 + */ + onBannerLoad(e) { + if (this.data.swiperHeight !== 400) return; // 只计算一次,避免多次抖动 + const { width, height } = e.detail; + const sysInfo = wx.getSystemInfoSync(); + // 减去左右padding (32rpx * 2) + const swiperWidth = sysInfo.windowWidth - (32 * 2 / 750 * sysInfo.windowWidth); + const ratio = width / height; + const swiperHeight = swiperWidth / ratio; + const swiperHeightRpx = swiperHeight * (750 / sysInfo.windowWidth); + + this.setData({ + swiperHeight: swiperHeightRpx + }); + }, + + /** + * 处理图片URL,如果是相对路径则拼接域名,并设置清晰度为85 + */ + processImageUrl(url) { + if (!url) return '' + let fullUrl = url + if (!url.startsWith('http://') && !url.startsWith('https://')) { + const baseUrl = 'https://ai-c.maimanji.com' + fullUrl = baseUrl + (url.startsWith('/') ? '' : '/') + url + } + + // 添加清晰度参数 q=85 + if (fullUrl.includes('?')) { + if (!fullUrl.includes('q=')) { + fullUrl += '&q=85' + } + } else { + fullUrl += '?q=85' + } + return fullUrl + }, + + /** + * 加载合作商家列表 + */ + async loadListData(reset = true) { + if (this.data.isLoading) return + if (!reset && !this.data.hasMore) return + + this.setData({ isLoading: true }) + const page = reset ? 1 : this.data.page + 1 + + try { + // 加载合作商家 (Merchants) + const res = await api.request('/service/providers', { + data: { page, limit: 20, type: 'merchant' } + }) + + let newData = [] + let total = 0 + + if (res.success && res.data) { + newData = res.data.providers || [] + total = res.data.total || 0 + newData = newData.map(item => ({ + ...item, + id: item.id, + image: this.processImageUrl(item.avatar), + tags: item.skills || item.service_types || [], + desc: item.desc || item.introduction || '暂无简介', + rating: item.rating || item.avg_rating || '5.0' + })) + } + + this.setData({ + listData: reset ? newData : [...this.data.listData, ...newData], + page, + hasMore: (page * 20) < total, + isLoading: false + }) + } catch (err) { + if (err.code === 404) { + console.warn('列表接口未部署 (404), 显示空列表') + } else { + console.error('加载列表数据失败', err) + } + this.setData({ + listData: reset ? [] : this.data.listData, + isLoading: false, + hasMore: false + }) + } + }, + + /** + * 页面上拉触底事件的处理函数 + */ + onReachBottom() { + this.loadListData(false) + }, + + onShow() { + wx.hideTabBar({ animation: false }) + const app = getApp() + this.setData({ + auditStatus: app.globalData.auditStatus + }) + this.loadListData() + this.loadUnreadCount() + }, + + /** + * 加载服务页Banner + * 调用专用API,只返回在线的Banner,按排序顺序 + */ + async loadBanners() { + try { + const res = await api.pageAssets.getServiceBanners() + + if (res && res.success && res.data) { + // 提取URL数组(API已按排序返回,已过滤下线的) + const banners = res.data.map(item => this.processImageUrl(item.asset_url)) + + if (banners.length > 0) { + this.setData({ banners }) + } else { + // 如果全部下线,使用默认配置 + this.setDefaultBanners() + } + } else { + this.setDefaultBanners() + } + } catch (err) { + if (err.code !== 404) { + console.error('加载Banner失败:', err) + } + this.setDefaultBanners() + } + }, + + /** + * 设置默认Banner(降级方案 - 使用CDN URL) + */ + setDefaultBanners() { + const cdnBase = 'https://ai-c.maimanji.com/images' + this.setData({ + banners: [ + `${cdnBase}/service-banner-1.png`, + `${cdnBase}/service-banner-2.png`, + `${cdnBase}/service-banner-3.png`, + `${cdnBase}/service-banner-4.png`, + `${cdnBase}/service-banner-5.png`, + `${cdnBase}/service-banner-6.png` + ] + }) + console.log('使用默认Banner配置') + }, + + /** + * 加载未读消息数 + */ + loadUnreadCount() { + if (!api.chat || typeof api.chat.getConversations !== 'function') return + + api.chat.getConversations().then(res => { + if (res && res.success && res.data) { + let count = 0 + res.data.forEach(item => { + count += (item.unread_count || 0) + }) + this.setData({ totalUnread: count }) + } + }).catch(err => { + // 静默处理未读数获取失败 + if (err.code !== 404) { + console.warn('获取未读数静默失败:', err) + } + }) + }, + + /** + * 点击服务类型 + */ + onServiceType(e) { + const id = e.currentTarget.dataset.id + + if (id === 'points') { + // 礼品商城 - 跳转到兑换商城 + wx.navigateTo({ url: '/pages/gift-shop/gift-shop' }) + } else if (id === 'merchants') { + // 合作商家 - 刷新列表 + this.loadListData() + } else if (id === 'eldercare') { + // 智慧康养 - 跳转到详情页 + wx.navigateTo({ url: '/pages/eldercare/eldercare' }) + } else if (id === 'custom') { + // 定制服务 - 跳转到详情页 + wx.navigateTo({ url: '/pages/custom/custom' }) + } else if (id === 'academy') { + // 心伴学堂 - 跳转到列表页 + wx.navigateTo({ url: '/pages/academy/list/list' }) + } else if (id === 'brand') { + // 关于品牌 - 跳转到品牌详情页 + wx.navigateTo({ url: '/pages/brand/brand' }) + } + }, + + /** + * 点击列表项 - 跳转商家详情 + */ + onItemTap(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/service-provider-detail/service-provider-detail?id=${id}` + }) + }, + + /** + * 切换Tab + */ + switchTab(e) { + const path = e.currentTarget.dataset.path + if (path) { + const app = getApp() + + // 消息页面需要登录 + if (path === '/pages/chat/chat') { + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ + url: '/pages/login/login?redirect=' + encodeURIComponent(path) + }) + return + } + } + wx.switchTab({ url: path }) + } + } +}) diff --git a/pages/service/service.json b/pages/service/service.json new file mode 100644 index 0000000..cd29add --- /dev/null +++ b/pages/service/service.json @@ -0,0 +1,5 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "服务", + "navigationStyle": "custom" +} diff --git a/pages/service/service.wxml b/pages/service/service.wxml new file mode 100644 index 0000000..56ccc8b --- /dev/null +++ b/pages/service/service.wxml @@ -0,0 +1,98 @@ + + + + + + 综合服务 + + + + + + + + + + + + + {{item.name}} + + + + + + + 精选服务 + + + + + + + + + + + + + + + + {{item.name}} + + + {{item.rating || '5.0'}} + + + {{item.desc}} + + {{tag}} + + + + + + + + 暂无合作商家 + + + + + + + + + + + + 陪伴 + + + + 文娱 + + + + 服务 + + + + + + + 消息 + + + + 我的 + + + diff --git a/pages/service/service.wxss b/pages/service/service.wxss new file mode 100644 index 0000000..8312051 --- /dev/null +++ b/pages/service/service.wxss @@ -0,0 +1,570 @@ +/* 服务页面样式 - 按照Figma设计实现 */ +page { + width: 100%; + overflow-x: hidden; + background: #fff; +} + +.page-container { + min-height: 100vh; + display: flex; + flex-direction: column; + width: 100%; + overflow-x: hidden; + background: #fff; +} + +/* 顶部导航栏已移除,改用全局 unified-header */ + +/* 内容滚动区域 */ +.content-scroll { + flex: 1; + width: 100%; +} + +/* 搜索栏 - 常规样式 */ +.search-section { + display: flex; + align-items: center; + padding: 0 32rpx; + margin-top: 16rpx; +} + +.search-bar { + flex: 1; + display: flex; + align-items: center; + height: 80rpx; + background: #F5F7FA; + border-radius: 40rpx; + padding: 0 24rpx; +} + +.search-icon { + width: 32rpx; + height: 32rpx; + flex-shrink: 0; + opacity: 0.5; +} + +.search-input-wrap { + flex: 1; + margin: 0 16rpx; +} + +.search-input { + width: 100%; + height: 80rpx; + font-size: 28rpx; + color: #101828; +} + +.placeholder { + color: #99A1AF; + font-size: 28rpx; +} + +.search-btn { + padding: 12rpx 32rpx; + background: #914584; + border-radius: 32rpx; + color: #fff; + font-size: 28rpx; + font-weight: 600; + flex-shrink: 0; +} + +/* 轮播图 Banner */ +.banner-section { + padding: 24rpx 32rpx; +} + +.banner-swiper { + width: 100%; + height: 300rpx; + border-radius: 32rpx; + overflow: hidden; + box-shadow: 0 8rpx 24rpx -8rpx rgba(243, 244, 246, 1), 0 20rpx 30rpx -6rpx rgba(243, 244, 246, 1); +} + +/* 防止轮播图闪烁 */ +swiper-item { + will-change: transform; + backface-visibility: hidden; + -webkit-backface-visibility: hidden; +} + +.banner-image { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + /* 防止图片闪烁 */ + backface-visibility: hidden; + -webkit-backface-visibility: hidden; + transform: translateZ(0); + -webkit-transform: translateZ(0); +} + +/* 服务类型 6宫格 - 按Figma设计: 358.77x239.94, 3列2行 */ +.service-types { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + width: 718rpx; + margin: 0 auto; + padding: 24rpx 0; + row-gap: 64rpx; +} + +.service-type-item { + width: 218rpx; + height: 208rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + gap: 24rpx; + box-sizing: border-box; +} + +.service-type-icon { + width: 168rpx; + height: 168rpx; + border-radius: 9999rpx; + flex-shrink: 0; +} + +.service-type-name { + font-family: Arial, sans-serif; + font-size: 36rpx; + font-weight: 700; + color: #364153; + line-height: 1.56; + text-align: center; +} + +/* 精选服务区域 */ +.featured-section { + background: #fff; + border-top: 2rpx solid #F9FAFB; + padding: 48rpx 32rpx 0; +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 32rpx; +} + +.section-title { + font-family: Arial, sans-serif; + font-size: 40rpx; + font-weight: 900; + color: #101828; + line-height: 1.33; +} + +.view-more { + display: flex; + align-items: center; + gap: 0; +} + +.more-text { + font-family: Arial, sans-serif; + font-size: 32rpx; + color: #6A7282; +} + +.arrow-icon { + width: 40rpx; + height: 40rpx; +} + +/* 分类标签 */ +.category-tabs { + width: 100%; + white-space: nowrap; + margin-bottom: 8rpx; +} + +.tabs-container { + display: inline-flex; + gap: 64rpx; +} + +.tab-item { + display: inline-flex; + flex-direction: column; + align-items: center; + position: relative; + padding-bottom: 8rpx; +} + +.tab-text { + font-family: Arial, sans-serif; + font-size: 32rpx; + color: #4A5565; +} + +.tab-item.active .tab-text { + color: #914584; +} + +.tab-indicator { + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 40rpx; + height: 4rpx; + background: #914584; + border-radius: 9999rpx; +} + +/* 服务人员列表 */ +.provider-list { + padding: 32rpx; +} + +.provider-card { + display: flex; + background: #fff; + border: 2rpx solid #F3F4F6; + border-radius: 32rpx; + padding: 32rpx; + margin-bottom: 32rpx; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04); +} + +/* 左侧头像区域 */ +.provider-left { + width: 168rpx; + flex-shrink: 0; + margin-right: 32rpx; + position: relative; +} + +.provider-top-tags { + display: flex; + flex-direction: column; + align-items: center; + gap: 8rpx; + margin-bottom: 16rpx; +} + +.experience-tag { + width: 100%; + height: 40rpx; + background: #F9FAFB; + border-radius: 16rpx; + font-family: Arial, sans-serif; + font-size: 24rpx; + font-weight: 700; + color: #364153; + display: flex; + align-items: center; + justify-content: center; + letter-spacing: -2.5%; +} + +.rating-wrap { + display: flex; + align-items: center; + gap: 8rpx; +} + +.star-icon { + width: 20rpx; + height: 20rpx; +} + +.rating-text { + font-family: Arial, sans-serif; + font-size: 22rpx; + font-weight: 700; + color: #364153; +} + +.avatar-container { + display: flex; + flex-direction: column; + align-items: center; +} + +.avatar-wrapper { + width: 168rpx; + height: 168rpx; + border-radius: 32rpx; + background: #F3F4F6; + overflow: hidden; +} + +.provider-avatar { + width: 100%; + height: 100%; +} + +.verified-badge { + margin-top: -24rpx; + padding: 4rpx 18rpx; + background: #FFFAF0; + border: 2rpx solid #FFE4BA; + border-radius: 8rpx; + font-family: Arial, sans-serif; + font-size: 20rpx; + font-weight: 700; + color: #B7791F; + box-shadow: 0 2rpx 4rpx -2rpx rgba(0, 0, 0, 0.1), 0 2rpx 6rpx rgba(0, 0, 0, 0.1); + position: relative; + z-index: 1; +} + +/* 右侧信息区域 */ +.provider-right { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 24rpx; +} + +.provider-header { + display: flex; + align-items: center; + gap: 16rpx; + height: 56rpx; +} + +.provider-name { + font-family: Arial, sans-serif; + font-size: 40rpx; + font-weight: 900; + color: #101828; + line-height: 1.4; +} + +.provider-tag { + padding: 4rpx 12rpx; + background: linear-gradient(180deg, #914584 0%, #B066A3 100%); + border-radius: 16rpx; + font-family: Arial, sans-serif; + font-size: 20rpx; + color: #fff; + letter-spacing: 2.5%; +} + +.provider-desc { + font-family: Arial, sans-serif; + font-size: 24rpx; + color: #6A7282; + line-height: 1.33; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.provider-skills { + display: flex; + gap: 12rpx; + flex-wrap: wrap; +} + +.skill-tag { + height: 48rpx; + padding: 0 16rpx; + background: #F5F7FA; + border-radius: 12rpx; + font-family: Arial, sans-serif; + font-size: 24rpx; + color: #4A5565; + display: flex; + align-items: center; + justify-content: center; +} + +.skill-tag.has-icon { + gap: 8rpx; +} + +.skill-icon { + width: 24rpx; + height: 24rpx; +} + +.provider-footer { + display: flex; + align-items: flex-end; + justify-content: space-between; + padding-top: 16rpx; + border-top: 2rpx solid #F9FAFB; +} + +.price-section { + display: flex; + align-items: baseline; +} + +.price-symbol { + font-family: Arial, sans-serif; + font-size: 24rpx; + color: #FF6B00; +} + +.price-value { + font-family: Arial, sans-serif; + font-size: 40rpx; + font-weight: 900; + color: #FF6B00; + line-height: 1.4; +} + +.price-unit { + font-family: Arial, sans-serif; + font-size: 24rpx; + color: #6A7282; + margin-left: 4rpx; +} + +.book-btn { + width: 176rpx; + height: 64rpx; + background: linear-gradient(180deg, #FF6B6B 0%, #FF8787 100%); + border-radius: 9999rpx; + font-family: Arial, sans-serif; + font-size: 28rpx; + font-weight: 700; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2rpx 4rpx -2rpx #FFEDD4, 0 2rpx 6rpx #FFEDD4; +} + +/* 暂无内容提示 */ +.empty-tip { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 100rpx 0; + gap: 20rpx; +} + +.empty-tip image { + width: 200rpx; + height: 200rpx; + opacity: 0.5; +} + +.empty-tip text { + font-size: 28rpx; + color: #99A1AF; +} + +/* 商家评分样式 */ +.merchant-rating { + display: flex; + align-items: center; + gap: 8rpx; + margin-left: auto; +} + +.merchant-rating .star-icon { + width: 32rpx; + height: 32rpx; +} + +.merchant-rating .rating-value { + font-size: 36rpx; + font-weight: 700; + color: #FF6B00; +} + +/* 智慧康养状态标签 */ +.provider-status { + display: flex; + margin-top: 10rpx; +} + +.status-tag { + padding: 4rpx 16rpx; + background-color: #F3F4F6; + color: #99A1AF; + font-size: 22rpx; + border-radius: 8rpx; +} + +/* 底部占位 */ +.bottom-placeholder { + height: 200rpx; +} + + +/* 自定义底部导航栏 - 与其他页面统一 */ +.custom-tabbar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 194rpx; + background: #fff; + display: flex; + align-items: flex-start; + justify-content: space-around; + padding-top: 24rpx; + z-index: 999; + border-top: 2rpx solid #F3F4F6; +} + +.tabbar-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12rpx; + width: 150rpx; + height: 120rpx; +} + +.tabbar-icon { + width: 68rpx; + height: 68rpx; +} + +.tabbar-text { + font-family: Arial, sans-serif; + font-size: 40rpx; + font-weight: 700; + color: #A58AA5; + line-height: 1; +} + +.tabbar-text.active { + color: #B06AB3; +} + +.message-icon-wrapper { + position: relative; + width: 68rpx; + height: 68rpx; +} + +.message-icon-wrapper .tabbar-icon { + width: 68rpx; + height: 68rpx; +} + +.message-dot { + position: absolute; + top: -8rpx; + right: -8rpx; + width: 24rpx; + height: 24rpx; + background: #FB2C36; + border: 2rpx solid #fff; + border-radius: 50%; +} diff --git a/pages/settings/settings.js b/pages/settings/settings.js new file mode 100644 index 0000000..6245004 --- /dev/null +++ b/pages/settings/settings.js @@ -0,0 +1,72 @@ +const { login } = require('../../utils_new/auth'); +const config = require('../../config/index'); + +Page({ + data: { + baseUrl: 'https://ai-c.maimanji.com', + statusBarHeight: 20, + navBarHeight: 44, + totalNavHeight: 64 + }, + onLoad() { + const sys = wx.getSystemInfoSync(); + const menu = wx.getMenuButtonBoundingClientRect(); + const statusBarHeight = sys.statusBarHeight || 20; + const navBarHeight = menu.height + (menu.top - statusBarHeight) * 2; + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight: statusBarHeight + navBarHeight + }); + this.load(); + }, + onShow() { + const defaultBaseUrl = String(config.API_BASE_URL || '').replace(/\/api$/, '') || 'https://ai-c.maimanji.com'; + const baseUrl = wx.getStorageSync('baseUrl') || defaultBaseUrl; + this.setData({ baseUrl }); + }, + onBack() { + wx.navigateBack({ delta: 1 }); + }, + onBaseUrl(e) { + this.setData({ baseUrl: e.detail.value }); + }, + setOnline() { + this.setData({ baseUrl: 'https://ai-c.maimanji.com' }); + }, + setLocalhost() { + this.setData({ baseUrl: 'http://localhost:3000' }); + }, + save() { + const baseUrl = (this.data.baseUrl || '').trim().replace(/\/$/, ''); + if (!baseUrl.startsWith('http')) { + wx.showToast({ title: '请输入正确的URL', icon: 'none' }); + return; + } + wx.setStorageSync('baseUrl', baseUrl); + const app = getApp(); + if (app?.globalData) app.globalData.baseUrl = baseUrl; + wx.showToast({ title: '已保存', icon: 'success' }); + }, + logout() { + wx.removeStorageSync('token'); + const app = getApp(); + if (app?.globalData) app.globalData.token = ''; + wx.showToast({ title: '已退出', icon: 'success' }); + }, + async syncProfile() { + try { + const userInfo = await new Promise((resolve, reject) => { + wx.getUserProfile({ + desc: '用于完善个人信息', + success: (res) => resolve(res.userInfo), + fail: reject + }); + }); + await login(userInfo); + wx.showToast({ title: '已同步', icon: 'success' }); + } catch (e) { + wx.showToast({ title: '取消或失败', icon: 'none' }); + } + } +}); diff --git a/pages/settings/settings.json b/pages/settings/settings.json new file mode 100644 index 0000000..3153ca5 --- /dev/null +++ b/pages/settings/settings.json @@ -0,0 +1,5 @@ +{ + "usingComponents": { + "app-icon": "/components/icon/icon" + } +} diff --git a/pages/settings/settings.wxml b/pages/settings/settings.wxml new file mode 100644 index 0000000..a1c08fa --- /dev/null +++ b/pages/settings/settings.wxml @@ -0,0 +1,31 @@ + + + + + + + + 设置 + + + + + + + 接口地址 + + + + + + 用于切换测试/正式环境 + + + + + + + + + + diff --git a/pages/settings/settings.wxss b/pages/settings/settings.wxss new file mode 100644 index 0000000..ffff3e0 --- /dev/null +++ b/pages/settings/settings.wxss @@ -0,0 +1,141 @@ +.page { + min-height: 100vh; + background: #E8C3D4; +} + +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; + background: rgba(248, 249, 252, 0.75); + backdrop-filter: blur(20rpx) saturate(180%); + -webkit-backdrop-filter: blur(20rpx) saturate(180%); +} + +.status-bar { + background: transparent; +} + +.nav-bar { + display: flex; + align-items: center; + justify-content: center; + padding: 0 32rpx; + position: relative; +} + +.nav-back { + position: absolute; + left: 32rpx; + top: 50%; + transform: translateY(-50%); + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.back-icon { + width: 48rpx; + height: 48rpx; +} + +.nav-title { + font-size: 34rpx; + font-weight: 700; + color: #1A1A1A; +} + +.wrap { + padding: 24rpx; +} + +.card { + background: #ffffff; + border-radius: 40rpx; + padding: 28rpx; + box-shadow: 0 10rpx 20rpx rgba(17, 24, 39, 0.04); + margin-bottom: 24rpx; +} + +.field { + margin-bottom: 18rpx; +} + +.label { + display: block; + font-size: 24rpx; + font-weight: 900; + color: #111827; + margin-bottom: 12rpx; +} + +.input { + height: 88rpx; + border-radius: 24rpx; + background: #f9fafb; + padding: 0 24rpx; + font-size: 26rpx; + font-weight: 800; + color: #111827; +} + +.hint { + display: block; + margin-top: 10rpx; + font-size: 22rpx; + color: #9ca3af; + font-weight: 700; +} + +.presets { + display: flex; + gap: 12rpx; + margin-top: 16rpx; + flex-wrap: wrap; +} + +.preset { + padding: 14rpx 18rpx; + border-radius: 999rpx; + background: rgba(17, 24, 39, 0.06); + color: #111827; + font-size: 24rpx; + font-weight: 800; +} + +.save { + width: 100%; + padding: 26rpx 0; + border-radius: 24rpx; + background: #b06ab3; + color: #ffffff; + font-size: 32rpx; + font-weight: 900; + box-shadow: 0 22rpx 44rpx rgba(176, 106, 179, 0.25); +} + +.logout { + width: 100%; + padding: 26rpx 0; + border-radius: 24rpx; + background: rgba(239, 68, 68, 0.1); + color: #ef4444; + font-size: 32rpx; + font-weight: 900; + margin-bottom: 16rpx; +} + +.login { + width: 100%; + padding: 26rpx 0; + border-radius: 24rpx; + background: rgba(176, 106, 179, 0.1); + color: #b06ab3; + font-size: 32rpx; + font-weight: 900; +} diff --git a/pages/singles-party/singles-party.js b/pages/singles-party/singles-party.js new file mode 100644 index 0000000..86b55e1 --- /dev/null +++ b/pages/singles-party/singles-party.js @@ -0,0 +1,326 @@ +// pages/singles-party/singles-party.js - 单身聚会页面 +const api = require('../../utils/api') +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + loading: false, + + // 活动标签 + activeTab: 'featured', // featured: 精选活动, free: 免费活动, vip: VIP活动, svip: SVIP活动 + + // 活动列表 + activityList: [], + + // 二维码弹窗 + showQrcodeModal: false, + qrcodeImageUrl: '' // 单身聚会群二维码 + }, + + onLoad() { + // 计算导航栏高度 + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight + }) + + this.loadActivityList() + }, + + /** + * 返回上一页 + */ + onBack() { + wx.navigateBack() + }, + + /** + * 切换活动标签 + */ + onTabChange(e) { + const tab = e.currentTarget.dataset.tab + if (tab === this.data.activeTab) return + + this.setData({ activeTab: tab }) + this.loadActivityList() + }, + + /** + * 加载活动列表 - 根据category筛选单身聚会 + */ + async loadActivityList() { + this.setData({ loading: true }) + + try { + const { activeTab } = this.data + const params = { + category: 'city', // 单身聚会通常属于同城活动 + limit: 100 + } + + // 根据选中的标签添加筛选条件 + if (activeTab === 'featured') { + params.tab = 'featured' + } else if (activeTab === 'free') { + params.priceType = 'free' + } else if (activeTab === 'vip') { + params.is_vip = true + } else if (activeTab === 'svip') { + params.is_svip = true + } + + const res = await api.activity.getList(params) + + if (res.success && res.data && res.data.list) { + // 前端筛选:显示categoryName包含"单身"或"聚会"的活动 + const allActivities = res.data.list + const filteredActivities = allActivities.filter(item => + item.categoryName === '单身聚会' || + (item.title && (item.title.includes('单身') || item.title.includes('聚会'))) + ) + + // 获取第一个活动的二维码作为俱乐部二维码(如果有) + let clubQrcode = '' + const firstWithQrcode = filteredActivities.find(item => item.activityGuideQrcode || item.activity_guide_qrcode) + if (firstWithQrcode) { + clubQrcode = firstWithQrcode.activityGuideQrcode || firstWithQrcode.activity_guide_qrcode + } + + // 转换数据格式 + const activityList = filteredActivities.map(item => { + const heat = item.heat || (item.likes * 2 + (item.views || 0) + (item.current_participants || 0) * 3) + + return { + id: item.id, + title: item.title, + date: this.formatDate(item.start_date || item.activityDate), + location: item.location || '', + venue: item.venue || '', + image: item.coverImage || item.cover_image || '', + heat: Math.floor(heat), + price: item.price_text || item.priceText || '免费', + priceType: item.is_free || item.priceType === 'free' ? 'free' : 'paid', + likes: item.likes || item.likesCount || 0, + participants: item.current_participants || item.currentParticipants || 0, + isLiked: item.is_liked || item.isLiked || false, + isSignedUp: item.is_registered || item.isSignedUp || false, + status: item.status || (item.currentParticipants >= item.maxParticipants && item.maxParticipants > 0 ? 'full' : 'upcoming'), + activityGuideQrcode: item.activityGuideQrcode || item.activity_guide_qrcode || '' + } + }) + + console.log('[singles-party] 加载成功,数量:', activityList.length) + this.setData({ + activityList, + qrcodeImageUrl: clubQrcode || this.data.qrcodeImageUrl || 'https://ai-c.maimanji.com/images/qrcode-singles-party.jpg' + }) + } else { + this.setData({ activityList: [] }) + } + } catch (err) { + console.error('加载活动列表失败', err) + wx.showToast({ + title: '加载失败', + icon: 'none' + }) + this.setData({ activityList: [] }) + } finally { + this.setData({ loading: false }) + } + }, + + /** + * 格式化日期 + */ + formatDate(dateStr) { + if (!dateStr) return '' + const date = new Date(dateStr) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}年${month}月${day}日` + }, + + /** + * 点击活动卡片 + */ + onActivityTap(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/activity-detail/activity-detail?id=${id}` + }) + }, + + /** + * 点赞 + */ + async onLike(e) { + const id = e.currentTarget.dataset.id + const index = e.currentTarget.dataset.index + + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ url: '/pages/login/login' }) + return + } + + try { + const res = await api.activity.toggleLike(id) + if (res.success) { + this.setData({ + [`activityList[${index}].isLiked`]: res.data.isLiked, + [`activityList[${index}].likes`]: res.data.likesCount + }) + } + } catch (err) { + console.error('点赞失败', err) + wx.showToast({ title: '操作失败', icon: 'none' }) + } + }, + + /** + * 立即报名 + */ + async onSignUp(e) { + const id = e.currentTarget.dataset.id + const index = e.currentTarget.dataset.index + + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ url: '/pages/login/login' }) + return + } + + const activity = this.data.activityList[index] + + // 检查活动状态 + if (activity.status === 'full' || activity.status === 'ended') { + const qrCode = activity.activityGuideQrcode || activity.activity_guide_qrcode || this.data.qrcodeImageUrl || 'https://ai-c.maimanji.com/api/common/qrcode?type=group' + this.setData({ + qrcodeImageUrl: qrCode, + showQrcodeModal: true + }) + return + } + + try { + if (activity.isSignedUp) { + // 取消报名 + const res = await api.activity.cancelSignup(id) + if (res.success) { + wx.showToast({ title: '已取消报名', icon: 'success' }) + this.loadActivityList() + } + } else { + // 报名 + const res = await api.activity.signup(id) + if (res.success) { + wx.showToast({ title: '报名成功', icon: 'success' }) + this.loadActivityList() + } else { + // 检查是否需要显示二维码 + if (res.code === 'QR_CODE_REQUIRED' || res.error === 'QR_CODE_REQUIRED' || res.code === 'ACTIVITY_ENDED' || res.error === '活动已结束') { + if (activity.activityGuideQrcode || activity.activity_guide_qrcode) { + this.setData({ qrcodeImageUrl: activity.activityGuideQrcode || activity.activity_guide_qrcode }) + } + this.setData({ showQrcodeModal: true }) + if (res.code === 'ACTIVITY_ENDED' || res.error === '活动已结束') { + wx.showToast({ title: '活动已结束,进群查看更多', icon: 'none' }) + } + } else { + wx.showToast({ + title: res.error || '操作失败', + icon: 'none' + }) + } + } + } + } catch (err) { + console.error('报名操作失败', err) + const isQrRequired = err && (err.code === 'QR_CODE_REQUIRED' || (err.data && err.data.code === 'QR_CODE_REQUIRED')) + const isActivityEnded = err && (err.code === 'ACTIVITY_ENDED' || (err.data && err.data.code === 'ACTIVITY_ENDED') || err.error === '活动已结束') + + if (isQrRequired || isActivityEnded) { + if (activity.activityGuideQrcode || activity.activity_guide_qrcode) { + this.setData({ qrcodeImageUrl: activity.activityGuideQrcode || activity.activity_guide_qrcode }) + } + this.setData({ showQrcodeModal: true }) + if (isActivityEnded) { + wx.showToast({ title: '活动已结束,进群查看更多', icon: 'none' }) + } + } else { + wx.showToast({ + title: err.error || err.message || '操作失败', + icon: 'none' + }) + } + } + }, + + /** + * 加入单身聚会群 + */ + onJoinGroup() { + if (!this.data.qrcodeImageUrl && this.data.activityList.length > 0) { + const firstWithQrcode = this.data.activityList.find(item => item.activityGuideQrcode || item.activity_guide_qrcode) + if (firstWithQrcode) { + this.setData({ qrcodeImageUrl: firstWithQrcode.activityGuideQrcode || firstWithQrcode.activity_guide_qrcode }) + } + } + this.setData({ showQrcodeModal: true }) + }, + + /** + * 关闭二维码弹窗 + */ + onCloseQrcodeModal() { + this.setData({ showQrcodeModal: false }) + }, + + /** + * 阻止冒泡 + */ + preventBubble() { + return + }, + + /** + * 保存二维码 + */ + async onSaveQrcode() { + try { + const { qrcodeImageUrl } = this.data + const downloadRes = await new Promise((resolve, reject) => { + wx.downloadFile({ + url: qrcodeImageUrl, + success: resolve, + fail: reject + }) + }) + + if (downloadRes.statusCode !== 200) throw new Error('下载失败') + + await new Promise((resolve, reject) => { + wx.saveImageToPhotosAlbum({ + filePath: downloadRes.tempFilePath, + success: resolve, + fail: reject + }) + }) + + wx.showToast({ title: '保存成功', icon: 'success' }) + this.onCloseQrcodeModal() + } catch (err) { + console.error('保存失败', err) + wx.showToast({ title: '保存失败', icon: 'none' }) + } + } +}) \ No newline at end of file diff --git a/pages/singles-party/singles-party.json b/pages/singles-party/singles-party.json new file mode 100644 index 0000000..b3c3b9f --- /dev/null +++ b/pages/singles-party/singles-party.json @@ -0,0 +1,7 @@ +{ + "navigationStyle": "custom", + "navigationBarTextStyle": "black", + "usingComponents": { + "app-icon": "../../components/icon/icon" + } +} \ No newline at end of file diff --git a/pages/singles-party/singles-party.wxml b/pages/singles-party/singles-party.wxml new file mode 100644 index 0000000..ad256cb --- /dev/null +++ b/pages/singles-party/singles-party.wxml @@ -0,0 +1,135 @@ + + + + + + + + + + 单身聚会 + + + + + + + + + 单身聚会俱乐部 + + 遇见缘分 + 告别单身 + + + + 点击立即加入 + + + + + + + + + 精选活动 + + + 免费活动 + + + VIP活动 + + + SVIP活动 + + + + + + + + + + + + + + {{item.price}} + + + + {{item.title}} + + + + {{item.date}} + + + + + {{item.location}} · {{item.venue}} + + + + {{item.heat}} + + + + + + + + {{item.participants}}人已报名 + + + + + + + + + 暂无活动 + + + + 没有更多活动了 ~ + + + + + + + + + + + + + 加入单身聚会群 + 遇见对的人,开启幸福人生 + + + + 长按二维码识别或保存 + 保存二维码 + + + \ No newline at end of file diff --git a/pages/singles-party/singles-party.wxss b/pages/singles-party/singles-party.wxss new file mode 100644 index 0000000..31f5557 --- /dev/null +++ b/pages/singles-party/singles-party.wxss @@ -0,0 +1,557 @@ +/* 单身聚会页面样式 - 浪漫粉色主题 */ +page { + background: linear-gradient(180deg, #FCE4EC 0%, #F8BBD0 100%); +} + +.page-container { + min-height: 100vh; + background: linear-gradient(180deg, #FCE4EC 0%, #F8BBD0 100%); +} + +/* 固定导航栏容器 */ +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; + background: rgba(255, 252, 253, 0.75); + backdrop-filter: blur(20rpx) saturate(180%); + -webkit-backdrop-filter: blur(20rpx) saturate(180%); +} + +/* 状态栏 */ +.status-bar { + background: transparent; +} + +/* 导航栏 */ +.nav-bar { + display: flex; + align-items: center; + justify-content: center; + padding: 0 32rpx; + background: transparent; + position: relative; +} + +.nav-back { + position: absolute; + left: 32rpx; + top: 50%; + transform: translateY(-50%); + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.back-icon { + width: 48rpx; + height: 48rpx; +} + +.nav-title { + font-size: 36rpx; + font-weight: 700; + color: #1A1A1A; + line-height: 1; +} + +/* 内容滚动区域 */ +.content-scroll { + height: 100vh; + box-sizing: border-box; +} + +/* 推广卡片 - 浪漫粉色渐变 */ +.city-group-card { + margin: 32rpx; + padding: 32rpx 40rpx; + min-height: 128rpx; + background: linear-gradient(135deg, + rgba(252, 228, 236, 0.6) 0%, + rgba(248, 187, 208, 0.6) 100%); + backdrop-filter: blur(16rpx) saturate(150%); + border: 2rpx solid rgba(236, 64, 122, 0.3); + border-radius: 48rpx; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 4rpx 20rpx rgba(236, 64, 122, 0.12); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.city-group-card:active { + transform: scale(0.98); + box-shadow: 0 2rpx 12rpx rgba(236, 64, 122, 0.18); +} + +.group-info { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8rpx; + padding-right: 24rpx; +} + +.group-title { + font-size: 40rpx; + font-weight: 700; + color: #1A1A1A; + line-height: 1.4; + white-space: nowrap; +} + +.group-tags { + display: flex; + flex-direction: column; + gap: 4rpx; +} + +.tag-item { + font-size: 28rpx; + font-weight: 500; + color: #4A5565; + line-height: 1.4; + white-space: nowrap; +} + +.join-btn { + padding: 0 40rpx; + height: 88rpx; + background: linear-gradient(135deg, #EC407A 0%, #D81B60 100%); + border-radius: 100rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + font-weight: 700; + color: #fff; + white-space: nowrap; + flex-shrink: 0; + box-shadow: 0 6rpx 24rpx rgba(236, 64, 122, 0.4), + 0 3rpx 12rpx rgba(236, 64, 122, 0.3); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; +} + +.join-btn:active { + transform: scale(0.96); + box-shadow: 0 4rpx 16rpx rgba(236, 64, 122, 0.45); +} + +/* 活动标签切换 - 横向滚动 */ +.tab-section { + padding: 32rpx 0; + background: transparent; + margin: 0 32rpx 32rpx; + position: relative; + z-index: 1; +} + +.tab-scroll { + width: 100%; + white-space: nowrap; +} + +.tab-scroll::-webkit-scrollbar { + display: none; +} + +.tab-list { + display: inline-flex; + gap: 20rpx; + padding: 0 4rpx; +} + +.tab-item { + padding: 20rpx 48rpx; + border-radius: 100rpx; + font-size: 32rpx; + font-weight: 700; + color: #6A7282; + background: rgba(255, 255, 255, 0.6); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + flex-shrink: 0; + white-space: nowrap; +} + +.tab-item:active { + transform: scale(0.96); +} + +.tab-item.active { + color: #fff; + background: linear-gradient(135deg, #EC407A 0%, #D81B60 100%); + box-shadow: 0 12rpx 24rpx rgba(236, 64, 122, 0.3); + transform: scale(1.02); +} + +/* 活动列表 - 毛玻璃卡片 */ +.activity-list { + padding: 0 32rpx; +} + +.activity-card { + margin-bottom: 32rpx; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(16rpx); + border-radius: 32rpx; + overflow: hidden; + box-shadow: 0 8rpx 32rpx rgba(236, 64, 122, 0.12), + 0 4rpx 16rpx rgba(236, 64, 122, 0.08); + border: 1rpx solid rgba(236, 64, 122, 0.15); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.activity-card:active { + transform: scale(0.98); + box-shadow: 0 4rpx 16rpx rgba(236, 64, 122, 0.15); +} + +/* 活动图片容器 */ +.activity-image-wrap { + position: relative; + width: 100%; + height: 360rpx; + overflow: hidden; + background: linear-gradient(135deg, #FCE4EC 0%, #F8BBD0 100%); +} + +.activity-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.activity-image-gradient { + width: 100%; + height: 100%; + background: linear-gradient(135deg, #FCE4EC 0%, #F8BBD0 100%); +} + +/* 点赞徽章 */ +.like-badge { + position: absolute; + top: 24rpx; + right: 24rpx; + display: flex; + align-items: center; + gap: 8rpx; + padding: 10rpx 20rpx; + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(10rpx); + border-radius: 100rpx; + z-index: 10; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); +} + +.like-count { + font-size: 24rpx; + color: #4A5565; + font-weight: 600; +} + +.like-badge.liked .like-count { + color: #FF5252; +} + +/* 价格标签 */ +.price-tag { + position: absolute; + bottom: 24rpx; + left: 24rpx; + padding: 10rpx 24rpx; + border-radius: 12rpx; + font-size: 24rpx; + font-weight: 700; + color: #FFFFFF; + z-index: 10; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15); +} + +.price-tag.free { + background: #4CAF50; +} + +.price-tag.paid { + background: #FF9800; +} + +/* 活动信息 */ +.activity-info { + padding: 40rpx; +} + +.activity-title { + font-size: 36rpx; + font-weight: 700; + color: #D81B60; + margin-bottom: 20rpx; + line-height: 1.4; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; +} + +.activity-meta { + display: flex; + align-items: center; + gap: 32rpx; + margin-bottom: 24rpx; +} + +.meta-item { + display: flex; + align-items: center; + gap: 8rpx; + font-size: 26rpx; + color: #EC407A; +} + +.meta-icon { + width: 28rpx; + height: 28rpx; +} + +.meta-text { + font-size: 26rpx; + color: #4A5565; +} + +.meta-row { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + margin-top: 8rpx; +} + +/* 活动底部 */ +.activity-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 24rpx; + border-top: 1rpx solid rgba(236, 64, 122, 0.1); +} + +.participants { + display: flex; + align-items: center; + gap: 12rpx; +} + +.avatar-stack { + display: flex; + align-items: center; +} + +.mini-avatar { + width: 48rpx; + height: 48rpx; + border-radius: 50%; + background: #FCE4EC; + border: 2rpx solid #fff; + margin-left: -12rpx; +} + +.mini-avatar:first-child { + margin-left: 0; +} + +.participant-text { + font-size: 24rpx; + color: #62748E; +} + +.heat-item { + margin-left: auto; +} + +.heat-text { + color: #FF9800; + font-weight: 600; +} + +/* 立即报名按钮 */ +.signup-btn { + width: 220rpx; + height: 72rpx; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #EC407A 0%, #D81B60 100%); + border-radius: 100rpx; + font-size: 28rpx; + font-weight: 700; + color: #FFFFFF; + box-shadow: 0 6rpx 20rpx rgba(236, 64, 122, 0.3); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + white-space: nowrap; + flex-shrink: 0; +} + +.signup-btn.signed { + background: #9CA3AF; + box-shadow: none; +} + +.signup-btn:active { + transform: scale(0.95); + box-shadow: 0 4rpx 12rpx rgba(236, 64, 122, 0.35); +} + +/* 空状态 */ +.empty-state { + padding: 120rpx 32rpx; + text-align: center; +} + +.empty-icon { + width: 200rpx; + height: 200rpx; + margin: 0 auto 32rpx; + opacity: 0.5; +} + +.empty-text { + font-size: 28rpx; + color: #F06292; +} + +/* 列表底部 */ +.list-footer { + padding: 40rpx 0; + text-align: center; +} + +.footer-text { + font-size: 24rpx; + color: #99A1AF; +} + +/* 二维码弹窗 */ +.qrcode-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + display: none; + align-items: center; + justify-content: center; +} + +.qrcode-modal.show { + display: flex; +} + +.modal-mask { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4rpx); +} + +.modal-content { + position: relative; + width: 680rpx; + background: #FFFFFF; + border-radius: 64rpx; + padding: 64rpx; + box-shadow: 0 50rpx 100rpx -24rpx rgba(0, 0, 0, 0.25); + display: flex; + flex-direction: column; + align-items: center; + z-index: 1; +} + +.close-btn { + position: absolute; + top: 32rpx; + right: 32rpx; + width: 72rpx; + height: 72rpx; + background: #F1F5F9; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.close-icon { + width: 40rpx; + height: 40rpx; +} + +.modal-title { + font-size: 48rpx; + font-weight: 700; + color: #1D293D; + text-align: center; + margin-bottom: 16rpx; + line-height: 1.5; +} + +.modal-subtitle { + font-size: 32rpx; + color: #62748E; + text-align: center; + margin-bottom: 48rpx; + line-height: 1.5; +} + +.qrcode-container { + width: 440rpx; + height: 440rpx; + background: #F8FAFC; + border: 2rpx solid #F1F5F9; + border-radius: 40rpx; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 48rpx; + overflow: hidden; +} + +.qrcode-image { + width: 404rpx; + height: 404rpx; + border-radius: 24rpx; +} + +.modal-tips { + font-size: 24rpx; + color: #999; + margin-bottom: 24rpx; +} + +.save-btn { + width: 552rpx; + height: 116rpx; + background: #EC407A; + border-radius: 100rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 40rpx; + font-weight: 700; + color: #FFFFFF; + box-shadow: 0 20rpx 30rpx -6rpx rgba(252, 228, 236, 1); + transition: all 0.3s ease; +} + +.save-btn:active { + transform: scale(0.96); +} \ No newline at end of file diff --git a/pages/square/square.js b/pages/square/square.js new file mode 100644 index 0000000..f70a3b0 --- /dev/null +++ b/pages/square/square.js @@ -0,0 +1,287 @@ +// pages/square/square.js - 微信朋友圈风格 +const app = getApp() +const api = require('../../utils/api') + +// 模拟动态数据 - 将从API获取,这里仅作为备用 +const MOCK_POSTS = [] + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + posts: [], + showCommentInput: false, + commentText: '', + currentCommentPostId: null, + showCreateModal: false, + newPostText: '', + newPostImages: [], + auditStatus: 0 + }, + + onShow() { + wx.hideTabBar({ animation: false }) + const app = getApp() + this.setData({ + auditStatus: app.globalData.auditStatus + }) + }, + + onLoad() { + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight + }) + + // 从API加载动态列表 + this.loadPosts() + }, + + /** + * 加载动态列表 + */ + async loadPosts() { + try { + const res = await api.post.getList({ page: 1, limit: 20 }) + + if (res.success && res.data) { + const posts = (res.data.list || res.data || []).map(post => ({ + id: post.id, + userId: post.user_id, + userName: post.user_name || '用户', + userAvatar: post.user_avatar || '', + content: post.content || '', + images: post.images || [], + time: post.created_at || '', + likes: post.likes || [], + comments: post.comments || [], + isLiked: post.is_liked || false, + likesText: (post.likes || []).join(','), + showActions: false + })) + + this.setData({ posts }) + } + } catch (err) { + console.error('加载动态失败', err) + // 如果API失败,使用空列表 + this.setData({ posts: [] }) + } + }, + + // 显示/隐藏操作面板 + onShowActions(e) { + const postId = e.currentTarget.dataset.id + const posts = this.data.posts.map(post => ({ + ...post, + showActions: post.id === postId ? !post.showActions : false + })) + this.setData({ posts }) + }, + + // 点击页面其他地方隐藏操作面板 + hideAllActions() { + const posts = this.data.posts.map(post => ({ + ...post, + showActions: false + })) + this.setData({ posts }) + }, + + onLikePost(e) { + const postId = e.currentTarget.dataset.id + const posts = this.data.posts.map(post => { + if (post.id === postId) { + const isLiked = !post.isLiked + let likes = [...post.likes] + + if (isLiked) { + likes.push('我') + } else { + likes = likes.filter(l => l !== '我') + } + + return { + ...post, + isLiked, + likes, + likesText: likes.join(','), + showActions: false + } + } + return { ...post, showActions: false } + }) + + this.setData({ posts }) + }, + + onCommentPost(e) { + const postId = e.currentTarget.dataset.id + // 先隐藏操作面板 + const posts = this.data.posts.map(post => ({ + ...post, + showActions: false + })) + + this.setData({ + posts, + showCommentInput: true, + currentCommentPostId: postId, + commentText: '' + }) + }, + + hideCommentInput() { + this.setData({ + showCommentInput: false, + currentCommentPostId: null + }) + }, + + onCommentInput(e) { + this.setData({ commentText: e.detail.value }) + }, + + onSendComment() { + const { commentText, currentCommentPostId, posts } = this.data + + if (!commentText.trim()) { + wx.showToast({ title: '请输入评论内容', icon: 'none' }) + return + } + + const newPosts = posts.map(post => { + if (post.id === currentCommentPostId) { + return { + ...post, + comments: [ + ...post.comments, + { id: 'c' + Date.now(), user: '我', text: commentText } + ] + } + } + return post + }) + + this.setData({ + posts: newPosts, + showCommentInput: false, + commentText: '', + currentCommentPostId: null + }) + + wx.showToast({ title: '评论成功', icon: 'success' }) + }, + + onPreviewImage(e) { + const { urls, current } = e.currentTarget.dataset + wx.previewImage({ + urls, + current + }) + }, + + onCreatePost() { + this.setData({ showCreateModal: true }) + }, + + hideCreateModal() { + this.setData({ + showCreateModal: false, + newPostText: '', + newPostImages: [] + }) + }, + + onNewPostInput(e) { + this.setData({ newPostText: e.detail.value }) + }, + + onAddImage() { + wx.chooseMedia({ + count: 9 - this.data.newPostImages.length, + mediaType: ['image'], + sourceType: ['album', 'camera'], + success: (res) => { + const newImages = res.tempFiles.map(f => f.tempFilePath) + this.setData({ + newPostImages: [...this.data.newPostImages, ...newImages] + }) + } + }) + }, + + onDeleteImage(e) { + const index = e.currentTarget.dataset.index + const images = this.data.newPostImages.filter((_, i) => i !== index) + this.setData({ newPostImages: images }) + }, + + async onSubmitPost() { + const { newPostText, newPostImages } = this.data + + if (!newPostText.trim() && newPostImages.length === 0) { + wx.showToast({ title: '请输入内容或添加图片', icon: 'none' }) + return + } + + try { + // 调用API发布动态 + const res = await api.post.create({ + content: newPostText, + images: newPostImages + }) + + if (res.success) { + this.setData({ + showCreateModal: false, + newPostText: '', + newPostImages: [] + }) + + wx.showToast({ title: '发布成功', icon: 'success' }) + + // 重新加载动态列表 + this.loadPosts() + } else { + wx.showToast({ title: res.message || '发布失败', icon: 'none' }) + } + } catch (err) { + console.error('发布动态失败', err) + wx.showToast({ title: '发布失败', icon: 'none' }) + } + }, + + // 切换 Tab - 需要登录的页面检查登录状态 + switchTab(e) { + const path = e.currentTarget.dataset.path + const app = getApp() + + // 消息和我的页面需要登录 + if (path === '/pages/chat/chat' || path === '/pages/profile/profile') { + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ + url: '/pages/login/login?redirect=' + encodeURIComponent(path) + }) + return + } + } + wx.switchTab({ url: path }) + }, + + // 测试按钮 + onTest() { + wx.showToast({ + title: '测试功能', + icon: 'none' + }) + } +}) diff --git a/pages/square/square.json b/pages/square/square.json new file mode 100644 index 0000000..b16ef3e --- /dev/null +++ b/pages/square/square.json @@ -0,0 +1,4 @@ +{ + "navigationStyle": "custom", + "usingComponents": {} +} diff --git a/pages/square/square.wxml b/pages/square/square.wxml new file mode 100644 index 0000000..4c0cba7 --- /dev/null +++ b/pages/square/square.wxml @@ -0,0 +1,191 @@ + + + + + + + + 广场 + + + + + + + + + + + + + + + + + + + + {{item.userName}} + + + + {{item.content}} + + + + + + + + + + + + + + + {{item.isLiked ? '取消' : '赞'}} + + + + + 评论 + + + + + + + + + + + + {{comment.user}} + + 回复 + {{comment.replyTo}} + + + {{comment.text}} + + + + + + + + + + + + + + + + + 取消 + 写评论 + 发送 + + + + + + + + + + + {{commentText.length || 0}}/500 + + + + + + + + + + 取消 + 发表动态 + 发表 + + + + + + × + + + + + + + + 添加图片 + + + + + + + + + 文娱 + + + + 陪聊 + + + + + + 聊天 + + + + 广场 + + + + 我的 + + + diff --git a/pages/square/square.wxss b/pages/square/square.wxss new file mode 100644 index 0000000..2361a4a --- /dev/null +++ b/pages/square/square.wxss @@ -0,0 +1,610 @@ +/* 广场页面样式 - 微信朋友圈风格 */ +.page-container { + min-height: 100vh; + background: #fff; +} + +/* 自定义导航栏 */ +.custom-nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + background: #ededed; + border-bottom: 1rpx solid #d9d9d9; +} + +.nav-bar { + height: 88rpx; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 32rpx; +} + +.nav-left, .nav-right { + width: 80rpx; +} + +.nav-title { + font-size: 34rpx; + font-weight: 600; + color: #000; +} + +.nav-right { + display: flex; + justify-content: flex-end; +} + +.create-btn { + width: 56rpx; + height: 56rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.create-icon { + width: 48rpx; + height: 48rpx; +} + +/* 内容滚动区域 */ +.content-scroll { + height: 100vh; + background: #fff; +} + +/* 动态列表 */ +.post-list { + padding-bottom: 220rpx; +} + +.post-item { + display: flex; + padding: 32rpx 32rpx 0; + border-bottom: 1rpx solid #ededed; +} + +/* 头像 */ +.post-avatar { + width: 84rpx; + height: 84rpx; + border-radius: 8rpx; + flex-shrink: 0; + margin-right: 20rpx; +} + +/* 右侧内容区 */ +.post-main { + flex: 1; + min-width: 0; + padding-bottom: 24rpx; +} + +/* 用户名 - 微信蓝色链接样式 */ +.post-username { + font-size: 32rpx; + font-weight: 600; + color: #576b95; + line-height: 1.4; + display: block; + margin-bottom: 8rpx; +} + +/* 内容文字 */ +.post-content { + margin-bottom: 16rpx; +} + +.post-text { + font-size: 30rpx; + color: #111; + line-height: 1.5; + word-break: break-all; +} + +/* 图片区域 - 九宫格布局 */ +.post-images { + display: flex; + flex-wrap: wrap; + gap: 8rpx; + margin-bottom: 16rpx; + max-width: 480rpx; +} + +.post-image { + border-radius: 6rpx; + background: #f5f5f5; +} + +/* 单张图片 */ +.post-images.images-1 .post-image { + width: 360rpx; + height: 360rpx; +} + +/* 两张图片 */ +.post-images.images-2 .post-image { + width: 236rpx; + height: 236rpx; +} + +/* 三张图片 */ +.post-images.images-3 .post-image { + width: 156rpx; + height: 156rpx; +} + +/* 四张图片 - 2x2 */ +.post-images.images-4 .post-image { + width: 236rpx; + height: 236rpx; +} + +/* 五张及以上 - 3列 */ +.post-images.images-5 .post-image, +.post-images.images-6 .post-image, +.post-images.images-7 .post-image, +.post-images.images-8 .post-image, +.post-images.images-9 .post-image { + width: 156rpx; + height: 156rpx; +} + +/* 底部信息栏 */ +.post-footer { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12rpx; +} + +.post-time { + font-size: 24rpx; + color: #b2b2b2; +} + +/* 操作按钮 - 两个小点 */ +.post-actions-btn { + padding: 12rpx 16rpx; + background: #f7f7f7; + border-radius: 6rpx; +} + +.action-dots { + display: flex; + gap: 6rpx; +} + +.dot { + width: 10rpx; + height: 10rpx; + background: #576b95; + border-radius: 50%; +} + +/* 操作面板 */ +.action-panel { + display: none; + background: #4c4c4c; + border-radius: 8rpx; + margin-bottom: 12rpx; + overflow: hidden; + flex-direction: row; +} + +.action-panel.show { + display: flex; +} + +.action-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 8rpx; + padding: 16rpx 24rpx; +} + +.action-btn-icon { + width: 32rpx; + height: 32rpx; + filter: brightness(0) invert(1); +} + +.action-btn-text { + font-size: 26rpx; + color: #fff; +} + +.action-divider { + width: 1rpx; + background: #666; + margin: 8rpx 0; +} + +/* 点赞和评论区域 */ +.post-interact { + background: #f7f7f7; + border-radius: 6rpx; + overflow: hidden; +} + +/* 点赞区域 */ +.likes-section { + display: flex; + align-items: flex-start; + padding: 12rpx 16rpx; + gap: 8rpx; +} + +.likes-icon { + width: 28rpx; + height: 28rpx; + flex-shrink: 0; + margin-top: 4rpx; +} + +.likes-text { + font-size: 28rpx; + color: #576b95; + line-height: 1.4; + word-break: break-all; +} + +/* 评论区域 */ +.comments-section { + padding: 0 16rpx 12rpx; + border-top: 1rpx solid #e5e5e5; +} + +.likes-section + .comments-section { + border-top: 1rpx solid #e5e5e5; +} + +.post-interact > .comments-section:first-child { + border-top: none; + padding-top: 12rpx; +} + +.comment-item { + padding: 6rpx 0; + font-size: 28rpx; + line-height: 1.5; +} + +.comment-user { + color: #576b95; +} + +.comment-reply { + color: #111; + margin: 0 4rpx; +} + +.comment-colon { + color: #111; +} + +.comment-text { + color: #111; + word-break: break-all; +} + +/* 底部占位 */ +.bottom-placeholder { + height: 48rpx; +} + +/* 评论弹窗 */ +.comment-modal { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + padding: 64rpx; +} + +.comment-modal-mask { + position: absolute; + inset: 0; + background: rgba(0,0,0,0.6); +} + +.comment-modal-content { + position: relative; + width: 100%; + max-width: 620rpx; + background: #fff; + border-radius: 24rpx; + overflow: hidden; + animation: fadeIn 0.2s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.comment-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 28rpx 32rpx; + border-bottom: 1rpx solid #e5e5e5; +} + +.comment-modal-cancel { + font-size: 32rpx; + color: #666; + padding: 8rpx 16rpx; +} + +.comment-modal-title { + font-size: 34rpx; + font-weight: 600; + color: #111; +} + +.comment-modal-send { + font-size: 32rpx; + color: #b2b2b2; + padding: 8rpx 16rpx; +} + +.comment-modal-send.active { + color: #576b95; + font-weight: 600; +} + +.comment-modal-body { + padding: 24rpx 32rpx; + min-height: 240rpx; +} + +.comment-textarea { + width: 100%; + min-height: 200rpx; + font-size: 32rpx; + line-height: 1.6; + color: #111; +} + +.comment-textarea::placeholder { + color: #b2b2b2; +} + +.comment-modal-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16rpx 32rpx 24rpx; + border-top: 1rpx solid #f0f0f0; +} + +.comment-toolbar { + display: flex; + gap: 24rpx; +} + +.toolbar-btn { + padding: 8rpx; +} + +.toolbar-icon { + width: 48rpx; + height: 48rpx; + opacity: 0.6; +} + +.comment-count { + font-size: 24rpx; + color: #b2b2b2; +} + +/* 发布动态弹窗 */ +.create-modal { + position: fixed; + inset: 0; + z-index: 300; +} + +.create-modal-mask { + position: absolute; + inset: 0; + background: rgba(0,0,0,0.5); +} + +.create-modal-content { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: #fff; + border-radius: 24rpx 24rpx 0 0; + padding-bottom: env(safe-area-inset-bottom); + max-height: 80vh; + overflow-y: auto; +} + +.create-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 28rpx 32rpx; + border-bottom: 1rpx solid #e5e5e5; +} + +.create-cancel { + font-size: 32rpx; + color: #111; +} + +.create-title { + font-size: 34rpx; + font-weight: 600; + color: #111; +} + +.create-submit { + font-size: 32rpx; + color: #b2b2b2; +} + +.create-submit.active { + color: #576b95; + font-weight: 600; +} + +.create-textarea { + width: 100%; + min-height: 240rpx; + padding: 24rpx 32rpx; + font-size: 32rpx; + line-height: 1.5; + box-sizing: border-box; +} + +.create-images { + display: flex; + flex-wrap: wrap; + gap: 16rpx; + padding: 0 32rpx 32rpx; +} + +.create-image-item { + position: relative; + width: calc(33.33% - 12rpx); + aspect-ratio: 1; +} + +.create-image { + width: 100%; + height: 100%; + border-radius: 8rpx; +} + +.create-image-delete { + position: absolute; + top: -12rpx; + right: -12rpx; + width: 40rpx; + height: 40rpx; + background: rgba(0,0,0,0.6); + color: #fff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; +} + +.create-image-add { + width: calc(33.33% - 12rpx); + aspect-ratio: 1; + background: #f7f7f7; + border-radius: 8rpx; + display: flex; + align-items: center; + justify-content: center; + border: 1rpx dashed #d9d9d9; +} + +.create-image-add-empty { + margin: 0 32rpx 32rpx; + padding: 48rpx; + background: #f7f7f7; + border-radius: 8rpx; + display: flex; + flex-direction: column; + align-items: center; + gap: 16rpx; + border: 1rpx dashed #d9d9d9; +} + +.add-icon { + width: 56rpx; + height: 56rpx; + opacity: 0.4; +} + +.add-text { + font-size: 28rpx; + color: #b2b2b2; +} + + +/* 自定义底部导航栏 */ +.custom-tabbar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 210rpx; + background: #fff; + border-radius: 60rpx 60rpx 0 0; + box-shadow: 0 -10rpx 30rpx rgba(0,0,0,0.04); + display: flex; + align-items: flex-start; + justify-content: space-around; + padding-top: 24rpx; + z-index: 999; +} + +.tabbar-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 8rpx; + width: 128rpx; +} + +.tabbar-icon { + width: 72rpx; + height: 72rpx; +} + +.tabbar-text { + font-size: 34rpx; + font-weight: 700; + color: #a58aa5; +} + +.tabbar-text.active { + color: #b06ab3; +} + +.tabbar-center { + display: flex; + flex-direction: column; + align-items: center; + gap: 8rpx; + margin-top: -80rpx; +} + +.center-btn { + width: 144rpx; + height: 144rpx; + background: linear-gradient(180deg, #c984cd 0%, #b06ab3 100%); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 0 0 12rpx #faf8fc, 0 16rpx 40rpx rgba(176,106,179,0.4); +} + +.center-icon { + width: 80rpx; + height: 80rpx; +} \ No newline at end of file diff --git a/pages/support/support.js b/pages/support/support.js new file mode 100644 index 0000000..ec1d2f9 --- /dev/null +++ b/pages/support/support.js @@ -0,0 +1,214 @@ +// pages/support/support.js +const app = getApp() +const api = require('../../utils/api') +const util = require('../../utils/util') +const imageUrl = require('../../utils/imageUrl') + +Page({ + data: { + statusBarHeight: 44, + navHeight: 96, + myAvatar: '/images/default-avatar.svg', + messages: [], + inputText: '', + inputFocus: false, + isTyping: false, + ticketId: '', + scrollIntoView: '', + scrollTop: 0, + pollingTimer: null + }, + + onLoad() { + const { statusBarHeight, navHeight, userInfo } = app.globalData + const myAvatar = imageUrl.getAvatarUrl(userInfo?.avatar) + + this.setData({ + statusBarHeight, + navHeight, + myAvatar + }) + + this.initSupport() + }, + + onAvatarError() { + this.setData({ + myAvatar: '/images/default-avatar.svg' + }) + }, + + onUnload() { + this.stopPolling() + }, + + onHide() { + this.stopPolling() + }, + + onShow() { + if (this.data.ticketId) { + this.startPolling() + } + }, + + /** + * 初始化客服会话 + */ + async initSupport() { + wx.showLoading({ title: '加载中...' }) + try { + const guestId = wx.getStorageSync('guestId') || util.generateId() + if (!wx.getStorageSync('guestId')) { + wx.setStorageSync('guestId', guestId) + } + + // 获取已有咨询列表 + const res = await api.customerService.getList(guestId) + const data = res.data || {} + const tickets = data.tickets || [] + + if (tickets.length > 0) { + // 使用最近的一个工单 + const latestTicket = tickets[0] + this.setData({ ticketId: latestTicket.id }) + await this.loadMessages(latestTicket.id) + } else { + // 如果没有工单,可以在首次发送消息时创建 + console.log('[support] No existing tickets found.') + } + } catch (err) { + console.error('[support] initSupport error:', err) + } finally { + wx.hideLoading() + this.startPolling() + } + }, + + /** + * 加载消息历史 + */ + async loadMessages(ticketId) { + try { + const res = await api.customerService.getDetail(ticketId) + if (res.success && res.data) { + const messages = res.data.messages.map(msg => ({ + id: msg.id, + isMe: msg.senderType === 'user', + text: msg.content, + time: util.formatTime(new Date(msg.createdAt), 'HH:mm'), + senderName: msg.senderName + })) + + // 如果有新消息才更新,避免闪烁 + if (JSON.stringify(messages) !== JSON.stringify(this.data.messages)) { + this.setData({ messages }, () => { + this.scrollToBottom() + }) + } + } + } catch (err) { + console.error('[support] loadMessages error:', err) + } + }, + + /** + * 发送消息 + */ + async onSend() { + const content = this.data.inputText.trim() + if (!content || this.isSending) return + + this.isSending = true + const tempId = util.generateId() + const now = new Date() + + // 先在本地显示 + const userMsg = { + id: tempId, + isMe: true, + text: content, + time: util.formatTime(now, 'HH:mm') + } + + this.setData({ + messages: [...this.data.messages, userMsg], + inputText: '', + inputFocus: true + }, () => { + this.scrollToBottom() + }) + + try { + if (this.data.ticketId) { + // 回复已有工单 + await api.customerService.reply({ + ticketId: this.data.ticketId, + content: content, + userName: app.globalData.userInfo?.nickname || '访客' + }) + } else { + // 创建新工单 + const guestId = wx.getStorageSync('guestId') + const res = await api.customerService.create({ + category: 'other', + content: content, + userName: app.globalData.userInfo?.nickname || '访客', + guestId: guestId + }) + if (res.success && res.data) { + this.setData({ ticketId: res.data.ticketId }) + } + } + // 发送后立即拉取一次 + if (this.data.ticketId) { + await this.loadMessages(this.data.ticketId) + } + } catch (err) { + console.error('[support] send message error:', err) + wx.showToast({ title: '发送失败', icon: 'none' }) + } finally { + this.isSending = false + } + }, + + onInput(e) { + this.setData({ inputText: e.detail.value }) + }, + + /** + * 开始轮询 + */ + startPolling() { + this.stopPolling() + this.data.pollingTimer = setInterval(() => { + if (this.data.ticketId) { + this.loadMessages(this.data.ticketId) + } + }, 4000) // 每4秒轮询一次 + }, + + /** + * 停止轮询 + */ + stopPolling() { + if (this.data.pollingTimer) { + clearInterval(this.data.pollingTimer) + this.data.pollingTimer = null + } + }, + + onBack() { + wx.navigateBack() + }, + + onTapChatArea() { + this.setData({ inputFocus: false }) + }, + + scrollToBottom() { + this.setData({ + scrollIntoView: 'chat-bottom-anchor' + }) + } +}) diff --git a/pages/support/support.json b/pages/support/support.json new file mode 100644 index 0000000..d41cf96 --- /dev/null +++ b/pages/support/support.json @@ -0,0 +1,7 @@ +{ + "usingComponents": { + "app-icon": "/components/icon/icon" + }, + "navigationBarTitleText": "在线客服", + "navigationStyle": "custom" +} \ No newline at end of file diff --git a/pages/support/support.wxml b/pages/support/support.wxml new file mode 100644 index 0000000..cc5c1b6 --- /dev/null +++ b/pages/support/support.wxml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + 返回 + + + + + + + + 在线客服 + + + + + + + + + + + + + + 您正在与人工客服对话,我们将尽快回复 + + + + + + + + + + + + + {{item.text}} + + + {{item.time}} + + + + + + + + + {{item.text}} + + {{item.time}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pages/support/support.wxss b/pages/support/support.wxss new file mode 100644 index 0000000..793e906 --- /dev/null +++ b/pages/support/support.wxss @@ -0,0 +1,310 @@ +/* pages/support/support.wxss */ + +/* 页面容器 */ +.page-container { + min-height: 100vh; + background: #F5F2FD; + display: flex; + flex-direction: column; + position: relative; +} + +/* 聊天区域包装器 */ +.chat-area-wrapper { + position: fixed; + left: 0; + right: 0; + bottom: 120rpx; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* 状态栏区域 */ +.status-bar-area { + position: fixed; + top: 0; + left: 0; + right: 0; + background: rgba(242, 237, 255, 0.6); + z-index: 101; +} + +/* 顶部导航栏 */ +.nav-header { + position: fixed; + left: 0; + right: 0; + background: rgba(255, 255, 255, 0.95); + border-bottom: 2rpx solid #F3F4F6; + z-index: 100; +} + +.nav-content { + display: flex; + align-items: center; + justify-content: space-between; + height: 98rpx; + padding: 0 16rpx; +} + +/* 返回按钮 */ +.nav-back { + display: flex; + align-items: center; + gap: 4rpx; + padding: 16rpx; + min-width: 160rpx; +} + +.back-icon { + width: 56rpx; + height: 56rpx; +} + +.back-text { + font-size: 34rpx; + font-weight: 700; + color: #914584; +} + +/* 中间角色信息 */ +.nav-center { + display: flex; + align-items: center; + gap: 16rpx; +} + +.nav-avatar-wrap { + width: 64rpx; + height: 64rpx; + border-radius: 50%; + overflow: hidden; + border: 2rpx solid #E5E7EB; + display: flex; + align-items: center; + justify-content: center; + background: #F3F4F6; +} + +.nav-avatar { + width: 80%; + height: 80%; +} + +.nav-name { + font-size: 34rpx; + font-weight: 700; + color: #101828; +} + +.online-dot { + width: 16rpx; + height: 16rpx; + background: #00C950; + border-radius: 50%; +} + +.nav-right-placeholder { + min-width: 160rpx; +} + +/* 聊天内容区域 */ +.chat-scroll { + height: 100%; + padding: 0 32rpx; + padding-top: 20rpx; + padding-bottom: 20rpx; + box-sizing: border-box; +} + +.chat-scroll::-webkit-scrollbar { + display: none; + width: 0; + height: 0; +} + +.encrypt-hint { + text-align: center; + padding: 32rpx 0 48rpx; +} + +.encrypt-hint text { + font-size: 24rpx; + color: #99A1AF; +} + +.chat-list { + display: flex; + flex-direction: column; + gap: 48rpx; +} + +.chat-item { + display: flex; + gap: 24rpx; + align-items: flex-start; +} + +.chat-item.me { + justify-content: flex-end; +} + +.avatar-wrap { + width: 88rpx; + height: 88rpx; + border-radius: 50%; + overflow: hidden; + flex-shrink: 0; + border: 2rpx solid rgba(255, 255, 255, 0.5); + box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1); + display: flex; + align-items: center; + justify-content: center; + background: #FFFFFF; +} + +.chat-avatar { + width: 100%; + height: 100%; +} + +.chat-item.other .chat-avatar { + width: 70%; + height: 70%; +} + +.message-content { + max-width: 540rpx; + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.message-content.me { + align-items: flex-end; +} + +.chat-bubble { + padding: 24rpx 40rpx; + word-break: break-all; + box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1); +} + +.chat-bubble.other { + background: #FFFFFF; + border-radius: 12rpx 44rpx 44rpx 44rpx; +} + +.chat-bubble.me { + background: #914584; + border-radius: 44rpx 12rpx 44rpx 44rpx; +} + +.chat-text { + font-size: 34rpx; + line-height: 1.625; +} + +.chat-bubble.other .chat-text { + color: #1E2939; +} + +.chat-bubble.me .chat-text { + color: #FFFFFF; +} + +.message-time { + font-size: 22rpx; + color: #99A1AF; + padding: 0 8rpx; +} + +.message-actions { + display: flex; + align-items: center; + gap: 16rpx; +} + +/* 正在输入动画 */ +.chat-bubble.typing { + display: flex; + gap: 12rpx; + padding: 28rpx 40rpx; +} + +.typing-dot { + width: 16rpx; + height: 16rpx; + background: #9CA3AF; + border-radius: 50%; + animation: typing 1.4s infinite ease-in-out; +} + +.typing-dot:nth-child(1) { animation-delay: 0s; } +.typing-dot:nth-child(2) { animation-delay: 0.2s; } +.typing-dot:nth-child(3) { animation-delay: 0.4s; } + +@keyframes typing { + 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; } + 30% { transform: translateY(-12rpx); opacity: 1; } +} + +.chat-bottom-space { + height: 40rpx; +} + +/* 底部输入区域 */ +.bottom-input-area { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: #FFFFFF; + border-top: 2rpx solid #F3F4F6; + box-shadow: 0 -10rpx 40rpx rgba(0, 0, 0, 0.03); + z-index: 100; + padding-bottom: env(safe-area-inset-bottom); +} + +.figma-input-container { + display: flex; + align-items: center; + gap: 16rpx; + padding: 24rpx 32rpx; + padding-bottom: 24rpx; +} + +.figma-input-wrap { + flex: 1; + background: #F9FAFB; + border: 2rpx solid #F3F4F6; + border-radius: 32rpx; + padding: 0 32rpx; + height: 96rpx; + display: flex; + align-items: center; +} + +.figma-text-input { + width: 100%; + height: 100%; + font-size: 34rpx; + color: #101828; +} + +.figma-send-btn { + width: 88rpx; + height: 88rpx; + background: #914584; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.figma-btn-icon { + width: 44rpx; + height: 44rpx; +} diff --git a/pages/team/team.js b/pages/team/team.js new file mode 100644 index 0000000..6925008 --- /dev/null +++ b/pages/team/team.js @@ -0,0 +1,147 @@ +const { request } = require('../../utils_new/request'); +const util = require('../../utils/util'); + +Page({ + data: { + statusBarHeight: 20, + navBarHeight: 44, + totalNavHeight: 64, + defaultAvatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=500&auto=format&fit=crop&q=60', + loading: true, + stats: { + todayReferrals: 0, + totalReferrals: 0, + totalContribution: '0.00' + }, + cardTitle: '守护会员', + list: [] + }, + onLoad() { + const sys = wx.getSystemInfoSync(); + const menu = wx.getMenuButtonBoundingClientRect(); + const statusBarHeight = sys.statusBarHeight || 20; + const navBarHeight = menu.height + (menu.top - statusBarHeight) * 2; + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight: statusBarHeight + navBarHeight + }); + this.load(); + }, + onBack() { + wx.navigateBack({ delta: 1 }); + }, + async load() { + this.setData({ loading: true }); + try { + try { + const statsRes = await request({ url: '/api/commission?action=stats', method: 'GET' }); + const statsBody = statsRes.data || {}; + if (statsBody.success) { + const d = statsBody.data || {}; + this.setData({ + stats: { + todayReferrals: Number(d.todayReferrals || d.today_referrals || 0), + totalReferrals: Number(d.totalReferrals || d.total_referrals || 0), + totalContribution: Number(d.totalContribution || d.total_contribution || 0).toFixed(2) + }, + cardTitle: this.getCardTitle(d.cardType || d.level || 'guardian_card') + }); + } + + const res = await request({ url: '/api/commission?action=referrals&page=1&pageSize=50', method: 'GET' }); + const body = res.data || {}; + + console.log('[团队页面] API响应:', JSON.stringify(body, null, 2)); + + // Flexible data extraction + let rawList = []; + if (Array.isArray(body.data)) { + rawList = body.data; + } else if (body.data && Array.isArray(body.data.list)) { + rawList = body.data.list; + } else if (body.list && Array.isArray(body.list)) { + rawList = body.list; + } + + console.log('[团队页面] rawList:', JSON.stringify(rawList.slice(0, 2), null, 2)); + + const roleMap = { + 'soulmate': '心伴会员', + 'guardian': '守护会员', + 'companion': '陪伴会员', + 'listener': '倾听会员', + 'partner': '城市合伙人' + }; + + const list = rawList.map((x) => { + const user = x.user || {}; + // Map fields robustly + let avatar = x.avatarUrl || x.avatar_url || x.userAvatar || user.avatarUrl || user.avatar_url || ''; + if (avatar) { + avatar = util.getFullImageUrl(avatar); + } + + const name = x.userName || x.nickname || x.nickName || user.nickname || user.nickName || ('用户' + (x.userId || x.id || '')); + const contribution = Number(x.totalContribution || x.total_contribution || x.amount || 0).toFixed(2); + const dateStr = x.boundAt || x.created_at || x.createdAt || Date.now(); + + // Get Member Level - 优先使用API返回的中文等级名称 + const levelText = x.userRoleName || user.userRoleName || roleMap[x.userRole] || roleMap[x.distributorRole] || roleMap[x.role] || (x.isDistributor ? '分销会员' : '普通用户'); + + return { + ...x, + userId: x.userId || x.id, + userAvatar: avatar || this.data.defaultAvatar, + userName: name, + levelText: levelText, + totalContribution: contribution, + boundAtText: this.formatDate(new Date(dateStr)) + }; + }); + + this.setData({ list }); + } catch (err) { + console.log('API failed, using mock data', err); + this.setData({ + stats: { todayReferrals: 2, totalReferrals: 15, totalContribution: '128.50' }, + list: [ + { userId: 1, userName: '小王', userAvatar: '', boundAtText: '2025-01-20 10:30:45', totalContribution: '12.50' }, + { userId: 2, userName: 'Alice', userAvatar: '', boundAtText: '2025-01-18 14:22:30', totalContribution: '30.00' }, + { userId: 3, userName: 'Bob', userAvatar: '', boundAtText: '2025-01-15 09:15:00', totalContribution: '5.00' } + ] + }); + } + } finally { + this.setData({ loading: false }); + } + }, + formatDate(d) { + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const h = String(d.getHours()).padStart(2, '0'); + const min = String(d.getMinutes()).padStart(2, '0'); + const s = String(d.getSeconds()).padStart(2, '0'); + return `${y}-${m}-${day} ${h}:${min}:${s}`; + }, + onAvatarError(e) { + const index = e.currentTarget.dataset.index; + if (index !== undefined) { + const list = this.data.list; + list[index].userAvatar = this.data.defaultAvatar; + this.setData({ list }); + } + }, + getCardTitle(type) { + const map = { + 'guardian_card': '守护会员', + 'companion_card': '陪伴会员', + 'identity_card': '身份会员', + 'vip': 'VIP会员', + 'partner': '城市合伙人' + }; + return map[type] || '守护会员'; + } +}); + diff --git a/pages/team/team.json b/pages/team/team.json new file mode 100644 index 0000000..3153ca5 --- /dev/null +++ b/pages/team/team.json @@ -0,0 +1,5 @@ +{ + "usingComponents": { + "app-icon": "/components/icon/icon" + } +} diff --git a/pages/team/team.wxml b/pages/team/team.wxml new file mode 100644 index 0000000..1df1d8b --- /dev/null +++ b/pages/team/team.wxml @@ -0,0 +1,61 @@ + + + + + + 返回 + + 我的团队 + + + + + + + + + + + {{cardTitle}} + + + + + 直推人数 + {{list.length}} + + + 团队总计 + {{stats.totalReferrals}} + + + + + + + + + 我的直推 + + + + + + 加载中... + 暂无直推成员 + + + + + + {{item.userName}} + {{item.levelText}} + + {{item.boundAtText}} + + + + + + + diff --git a/pages/team/team.wxss b/pages/team/team.wxss new file mode 100644 index 0000000..e755b47 --- /dev/null +++ b/pages/team/team.wxss @@ -0,0 +1,175 @@ +.page { + min-height: 100vh; + background: #F8F9FC; +} + +.wrap { + padding: 30rpx 32rpx; +} + +/* Guardian Card */ +.guardian-card { + background: linear-gradient(135deg, #CF91D3 0%, #B06AB3 100%); + border-radius: 48rpx; + padding: 40rpx; + color: #ffffff; + box-shadow: 0 20rpx 40rpx rgba(176, 106, 179, 0.25); + margin-bottom: 48rpx; + position: relative; + overflow: hidden; +} + +.guardian-card::after { + content: ""; + position: absolute; + top: -20rpx; + right: -20rpx; + width: 200rpx; + height: 200rpx; + background: rgba(255, 255, 255, 0.1); + border-radius: 50%; + filter: blur(40rpx); +} + +.guardian-header { + display: flex; + align-items: center; + gap: 20rpx; + margin-bottom: 50rpx; +} + +.icon-box { + width: 80rpx; + height: 80rpx; + background: rgba(255, 255, 255, 0.2); + border-radius: 24rpx; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(10rpx); +} + +.guardian-title { + font-size: 36rpx; + font-weight: 800; + letter-spacing: 2rpx; +} + +.stats-row { + display: flex; + justify-content: space-between; + padding: 0 20rpx; +} + +.stat-col { + display: flex; + flex-direction: column; +} + +.stat-label { + font-size: 26rpx; + color: rgba(255, 255, 255, 0.85); + margin-bottom: 16rpx; + font-weight: 500; +} + +.stat-num { + font-size: 64rpx; + font-weight: 900; + line-height: 1; +} + +/* Section Header */ +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 30rpx; + padding: 0 10rpx; +} + +.header-left { + display: flex; + align-items: center; + gap: 16rpx; +} + +.header-title { + font-size: 34rpx; + font-weight: 800; + color: #1F2937; +} + +/* Member List */ +.member-list { + display: flex; + flex-direction: column; + gap: 24rpx; +} + +.member-card { + background: #ffffff; + border-radius: 32rpx; + padding: 30rpx; + display: flex; + align-items: center; + gap: 24rpx; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.02); + border: 2rpx solid transparent; + transition: all 0.2s; +} + +.member-card:active { + transform: scale(0.98); +} + +.member-avatar { + width: 96rpx; + height: 96rpx; + border-radius: 50%; + background: #f3f4f6; + border: 4rpx solid #FDF4F9; +} + +.member-info { + flex: 1; + min-width: 0; +} + +.name-row { + display: flex; + align-items: center; + gap: 16rpx; + margin-bottom: 8rpx; +} + +.member-name { + font-size: 32rpx; + font-weight: 800; + color: #111827; +} + +.tag-badge { + background: #B06AB3; + color: #ffffff; + font-size: 20rpx; + font-weight: 700; + padding: 4rpx 12rpx; + border-radius: 999rpx; +} + +.member-meta { + font-size: 24rpx; + color: #6B7280; + font-weight: 500; +} + +.loading, +.empty { + text-align: center; + color: #9ca3af; + font-weight: 800; + padding: 100rpx 0; + font-size: 28rpx; +} + diff --git a/pages/theme-travel-apply/theme-travel-apply.js b/pages/theme-travel-apply/theme-travel-apply.js new file mode 100644 index 0000000..6a3c5b7 --- /dev/null +++ b/pages/theme-travel-apply/theme-travel-apply.js @@ -0,0 +1,162 @@ +// pages/theme-travel-apply/theme-travel-apply.js +// 定制主题申请页面 +const api = require('../../utils/api') + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + showForm: true, + applyStatus: 'none', + statusTitle: '', + statusDesc: '', + isReapply: false, + agreed: false, + formData: { + realName: '', + city: '', + phone: '', + remarks: '' + }, + canSubmit: false + }, + + onLoad(options) { + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight, + isReapply: options.isReapply === 'true' + }) + + this.checkApplyStatus() + }, + + goBack() { + wx.navigateBack() + }, + + async checkApplyStatus() { + const token = wx.getStorageSync('auth_token') + if (!token) { + this.setData({ applyStatus: 'none' }) + return + } + + try { + const res = await api.request('/theme-travel/apply') + if (res.success && res.data) { + const data = res.data + if (data.status === 'approved') { + this.setData({ + applyStatus: 'approved', + statusTitle: '申请已通过', + statusDesc: '恭喜您成为主题旅游服务师!' + }) + } else if (data.status === 'pending') { + this.setData({ + applyStatus: 'pending', + statusTitle: '审核中', + statusDesc: '您的申请正在审核中,请耐心等待' + }) + } else if (data.status === 'rejected') { + this.setData({ + applyStatus: 'rejected', + statusTitle: '申请未通过', + statusDesc: data.rejectReason || '很抱歉,您的申请未通过审核' + }) + } + } + } catch (err) { + console.log('获取申请状态失败:', err) + this.setData({ applyStatus: 'none' }) + } + }, + + reapply() { + this.setData({ isReapply: true, applyStatus: 'none' }) + }, + + onInputChange(e) { + const field = e.currentTarget.dataset.field + this.setData({ [`formData.${field}`]: e.detail.value }) + this.checkCanSubmit() + }, + + toggleAgreement() { + this.setData({ agreed: !this.data.agreed }) + this.checkCanSubmit() + }, + + viewAgreement() { + wx.navigateTo({ url: '/pages/agreement/agreement?code=cooperation_service' }) + }, + + checkCanSubmit() { + const { formData, agreed } = this.data + + const canSubmit = + formData.realName && + formData.phone && + formData.phone.length === 11 && + agreed + + this.setData({ canSubmit }) + }, + + async submitApply() { + if (!this.data.canSubmit) return + + const { formData } = this.data + + if (!/^1[3-9]\d{9}$/.test(formData.phone)) { + wx.showToast({ title: '请输入正确的手机号', icon: 'none' }) + return + } + + wx.showLoading({ title: '提交中...' }) + try { + const res = await api.request('/theme-travel/apply', { + method: 'POST', + data: { + realName: formData.realName, + city: formData.city, + phone: formData.phone, + remarks: formData.remarks + } + }) + + if (res.success || res.code === 0) { + wx.showToast({ title: '申请已提交', icon: 'success' }) + this.setData({ + applyStatus: 'pending', + statusTitle: '审核中', + statusDesc: '您的申请正在审核中,请耐心等待', + isReapply: false + }) + } else { + wx.showToast({ title: res.message || '提交失败', icon: 'none' }) + } + } catch (err) { + if (err.code === 404) { + wx.showModal({ + title: '提示', + content: '该服务申请即将开放,敬请期待!', + showCancel: false, + confirmColor: '#b06ab3' + }) + } else { + wx.showToast({ title: err.message || '提交失败', icon: 'none' }) + } + } finally { + wx.hideLoading() + } + } +}) diff --git a/pages/theme-travel-apply/theme-travel-apply.json b/pages/theme-travel-apply/theme-travel-apply.json new file mode 100644 index 0000000..266dac9 --- /dev/null +++ b/pages/theme-travel-apply/theme-travel-apply.json @@ -0,0 +1,4 @@ +{ + "navigationStyle": "custom", + "navigationBarTitleText": "主题旅游申请" +} diff --git a/pages/theme-travel-apply/theme-travel-apply.wxml b/pages/theme-travel-apply/theme-travel-apply.wxml new file mode 100644 index 0000000..56368ef --- /dev/null +++ b/pages/theme-travel-apply/theme-travel-apply.wxml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + 定制主题 + + + + + + + + + + + + + + + {{statusTitle}} + {{statusDesc}} + + + + + + + + + 基本信息 + + + + + 姓名 + * + + + + + + + + + 所在城市 + (选填) + + + + + + + + + 手机号 + * + + + + + + + + + + + 备注信息 + + + + + + {{formData.remarks.length || 0}}/500 + + + + + + + + + + 我已阅读并同意 + 《合作入驻服务协议》 + + + + + + + + + + + diff --git a/pages/theme-travel-apply/theme-travel-apply.wxss b/pages/theme-travel-apply/theme-travel-apply.wxss new file mode 100644 index 0000000..9bba1df --- /dev/null +++ b/pages/theme-travel-apply/theme-travel-apply.wxss @@ -0,0 +1,201 @@ +/* 主题旅游申请页面样式 - 玫瑰紫版 v3.0 */ +.page-container { + min-height: 100vh; + background: linear-gradient(180deg, #E8C3D4 0%, #F5E6ED 100%); +} + +/* 固定导航栏容器 */ +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; + background: rgba(248, 249, 252, 0.75); + backdrop-filter: blur(20rpx) saturate(180%); +} + +/* 状态栏 */ +.status-bar { + background: transparent; +} + +/* 导航栏 */ +.nav-bar { + display: flex; + align-items: center; + justify-content: center; + padding: 0 32rpx; + background: transparent; + position: relative; +} + +.nav-back { + position: absolute; + left: 32rpx; + top: 50%; + transform: translateY(-50%); + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.back-icon { + width: 48rpx; + height: 48rpx; +} + +.nav-title { + font-size: 36rpx; + font-weight: 700; + color: #1F2937; + line-height: 1; +} + +/* 内容滚动区域 */ +.content-scroll { + height: 100vh; + box-sizing: border-box; +} + +.intro-section { padding: 16px; } + +.banner-card { + height: 160px; + border-radius: 16px; + overflow: hidden; + margin-bottom: 16px; + background: linear-gradient(135deg, #914584 0%, #7A3A6F 100%); + box-shadow: 0 4rpx 16rpx rgba(145, 69, 132, 0.25); +} + +.banner-gradient { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.banner-title { font-size: 24px; font-weight: 600; color: #fff; margin-bottom: 8px; } +.banner-subtitle { font-size: 14px; color: rgba(255,255,255,0.9); } + +.info-card { + background: #fff; + border-radius: 16px; + padding: 20px; + margin-bottom: 16px; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06), + 0 2rpx 8rpx rgba(0, 0, 0, 0.04); + transition: transform 0.25s ease, box-shadow 0.25s ease; +} + +.card-header { display: flex; align-items: center; margin-bottom: 16px; } + +.card-icon { + width: 32px; + height: 32px; + background: linear-gradient(135deg, #7C3AED 0%, #A78BFA 100%); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 12px; + box-shadow: 0 2rpx 8rpx rgba(124, 58, 237, 0.2); +} + +.card-icon image { width: 20px; height: 20px; } +.card-title { font-size: 18px; font-weight: 600; color: #111827; } +.intro-text { font-size: 14px; color: #6B7280; line-height: 1.8; } + +.service-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } +.service-item { background: #f8f8f8; border-radius: 8px; padding: 12px 8px; text-align: center; transition: transform 0.2s ease; } +.service-item:active { transform: scale(0.95); } +.service-name { font-size: 13px; color: #111827; } + +.advantage-list { padding: 0; } +.advantage-item { display: flex; align-items: center; padding: 8px 0; } +.advantage-dot { width: 6px; height: 6px; background: linear-gradient(135deg, #7C3AED 0%, #A78BFA 100%); border-radius: 50%; margin-right: 12px; } +.advantage-text { font-size: 14px; color: #6B7280; } + +.apply-btn-area { padding: 24px 0; text-align: center; } +.apply-btn { width: 100%; height: 48px; background: linear-gradient(135deg, #7C3AED 0%, #A78BFA 100%); border-radius: 24px; color: #fff; font-size: 16px; font-weight: 500; border: none; margin-bottom: 12px; box-shadow: 0 4rpx 16rpx rgba(124, 58, 237, 0.35); transition: all 0.25s ease; } +.apply-btn:active { transform: scale(0.98); box-shadow: 0 2rpx 8rpx rgba(124, 58, 237, 0.4); } +.apply-tip { font-size: 13px; color: #9CA3AF; } + +.apply-form { padding: 16px; } +.status-card { background: #fff; border-radius: 16px; padding: 40px 24px; text-align: center; margin-bottom: 16px; } +.status-icon { width: 80px; height: 80px; margin: 0 auto 16px; } +.status-icon image { width: 100%; height: 100%; } +.status-title { display: block; font-size: 18px; font-weight: 600; color: #111827; margin-bottom: 8px; } +.status-desc { font-size: 14px; color: #6B7280; line-height: 1.6; } +.btn-secondary { margin-top: 24px; width: 160px; height: 44px; background: #fff; border: 1px solid #7C3AED; border-radius: 22px; color: #7C3AED; font-size: 15px; transition: all 0.2s ease; } +.btn-secondary:active { transform: scale(0.95); background: #FAF5FF; } + +.form-content { background: #fff; border-radius: 16px; padding: 24px 20px; box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06), + 0 2rpx 8rpx rgba(0, 0, 0, 0.04); } +.form-header { text-align: center; margin-bottom: 24px; } +.form-title { display: block; font-size: 20px; font-weight: 600; color: #111827; margin-bottom: 8px; } +.form-subtitle { font-size: 14px; color: #9CA3AF; } + +.form-section { margin-bottom: 24px; } +.section-header { display: flex; align-items: center; margin-bottom: 16px; } +.section-title { font-size: 16px; font-weight: 600; color: #111827; } +.required { color: #ff4d4f; margin-left: 4px; } + +.avatar-upload-area { display: flex; justify-content: center; margin-bottom: 8px; } +.avatar-circle { width: 100px; height: 100px; border-radius: 50%; background: linear-gradient(135deg, #E5E7EB 0%, #F3F4F6 100%); overflow: hidden; display: flex; align-items: center; justify-content: center; border: 2px solid #F1F5F9; } +.avatar-image { width: 100%; height: 100%; } +.upload-placeholder { text-align: center; } +.camera-icon { width: 32px; height: 32px; margin-bottom: 4px; } +.upload-text { font-size: 12px; color: #9CA3AF; } +.form-tip { font-size: 12px; color: #9CA3AF; margin-top: 8px; } + +.form-item { margin-bottom: 16px; } +.item-label-row { display: flex; align-items: center; margin-bottom: 8px; } +.item-label { font-size: 14px; color: #111827; } +.input-wrapper { background: #f8f8f8; border-radius: 8px; padding: 0 12px; transition: background 0.2s ease; } +.input-wrapper:focus-within { background: #F3F4F6; } +.item-input { width: 100%; height: 44px; font-size: 14px; color: #111827; } + +.gender-options { display: flex; gap: 12px; } +.gender-btn { flex: 1; height: 44px; background: #f8f8f8; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 14px; color: #6B7280; transition: all 0.2s ease; } +.gender-btn:active { transform: scale(0.95); } +.gender-btn.active { background: linear-gradient(135deg, #7C3AED 0%, #A78BFA 100%); color: #fff; font-weight: 500; box-shadow: 0 2rpx 8rpx rgba(124, 58, 237, 0.3); } + +.service-types { display: flex; flex-wrap: wrap; gap: 10px; } +.service-btn { padding: 8px 16px; background: #f8f8f8; border-radius: 20px; font-size: 13px; color: #6B7280; transition: all 0.2s ease; } +.service-btn:active { transform: scale(0.95); } +.service-btn.active { background: linear-gradient(135deg, #7C3AED 0%, #A78BFA 100%); color: #fff; box-shadow: 0 2rpx 8rpx rgba(124, 58, 237, 0.3); } + +.level-options { display: flex; gap: 10px; } +.level-btn { flex: 1; padding: 10px 8px; background: #f8f8f8; border-radius: 8px; font-size: 13px; color: #6B7280; text-align: center; transition: all 0.2s ease; } +.level-btn:active { transform: scale(0.95); } +.level-btn.active { background: linear-gradient(135deg, #7C3AED 0%, #A78BFA 100%); color: #fff; font-weight: 500; box-shadow: 0 2rpx 8rpx rgba(124, 58, 237, 0.3); } + +.cert-upload { width: 100%; height: 120px; background: linear-gradient(135deg, #E5E7EB 0%, #F3F4F6 100%); border-radius: 8px; border: 1px dashed #D1D5DB; display: flex; align-items: center; justify-content: center; overflow: hidden; transition: border-color 0.2s ease; } +.cert-upload:active { border-color: #7C3AED; } +.cert-image { width: 100%; height: 100%; } +.cert-placeholder { text-align: center; } +.upload-icon { width: 40px; height: 40px; margin-bottom: 8px; } + +.textarea-wrapper { background: #f8f8f8; border-radius: 8px; padding: 12px; } +.intro-textarea { width: 100%; height: 120px; font-size: 14px; color: #111827; line-height: 1.6; } +.textarea-footer { display: flex; justify-content: flex-end; margin-top: 8px; } +.char-count { font-size: 12px; color: #9CA3AF; } + +.agreement-row { display: flex; align-items: center; margin: 24px 0; } +.checkbox { width: 20px; height: 20px; border: 1px solid #D1D5DB; border-radius: 4px; margin-right: 8px; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; } +.checkbox.checked { background: linear-gradient(135deg, #7C3AED 0%, #A78BFA 100%); border-color: transparent; box-shadow: 0 2rpx 8rpx rgba(124, 58, 237, 0.3); } +.check-icon { width: 14px; height: 14px; } +.normal-text { font-size: 13px; color: #6B7280; } +.link-text { font-size: 13px; color: #7C3AED; } + +.submit-btn { width: 100%; height: 48px; background: linear-gradient(135deg, #7C3AED 0%, #A78BFA 100%); border-radius: 24px; color: #fff; font-size: 16px; font-weight: 500; border: none; box-shadow: 0 4rpx 16rpx rgba(124, 58, 237, 0.35); transition: all 0.25s ease; } +.submit-btn:active { transform: scale(0.98); box-shadow: 0 2rpx 8rpx rgba(124, 58, 237, 0.4); } +.submit-btn.disabled { opacity: 0.5; transform: none; } +.bottom-placeholder { height: 40px; } diff --git a/pages/theme-travel/theme-travel.js b/pages/theme-travel/theme-travel.js new file mode 100644 index 0000000..4fa628f --- /dev/null +++ b/pages/theme-travel/theme-travel.js @@ -0,0 +1,455 @@ +// pages/theme-travel/theme-travel.js - 高端定制页面 +const api = require('../../utils/api') +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + loading: false, + loadingMore: false, + activeTab: 'featured', + + // 活动列表 + activityList: [], + + // 分页相关 + page: 1, + limit: 20, + hasMore: true, + total: 0, + + // 二维码引导弹窗 + showQrcodeModal: false, + qrcodeImageUrl: 'https://ai-c.maimanji.com/api/common/qrcode?type=theme-travel' + }, + + onLoad(options) { + // 计算导航栏高度 + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight + }) + + this.loadActivityList() + }, + + /** + * 返回上一页 + */ + onBack() { + wx.navigateBack() + }, + + /** + * 加载活动列表(支持分页) + */ + async loadActivityList(isLoadMore = false) { + if (isLoadMore) { + this.setData({ loadingMore: true }) + } else { + this.setData({ loading: true, page: 1, hasMore: true, activityList: [] }) + } + + try { + const { activeTab, page, limit } = this.data + const params = { + category: 'travel', + limit: limit, + page: page + } + + if (activeTab === 'featured') { + params.tab = 'featured' + } else if (activeTab === 'free') { + params.priceType = 'free' + } else if (activeTab === 'vip') { + params.is_vip = true + } else if (activeTab === 'svip') { + params.is_svip = true + } + + const res = await api.activity.getList(params) + + if (res.success && res.data && res.data.list) { + const total = res.data.total || 0 + const allActivities = res.data.list + const travelActivities = allActivities.filter(item => item.categoryName === '高端定制') + + let clubQrcode = '' + const firstWithQrcode = travelActivities.find(item => item.activityGuideQrcode || item.activity_guide_qrcode) + if (firstWithQrcode && !isLoadMore) { + clubQrcode = firstWithQrcode.activityGuideQrcode || firstWithQrcode.activity_guide_qrcode + } + + const newActivityList = travelActivities.map(item => { + const heat = item.heat || (item.likes * 2 + (item.views || 0) + (item.current_participants || 0) * 3) + + return { + id: item.id, + title: item.title, + date: this.formatDate(item.start_date || item.activityDate, item.end_date || item.endDate), + location: item.location || '', + venue: item.venue || '', + image: item.coverImage || item.cover_image || '', + heat: Math.floor(heat), + price: item.price_text || item.priceText || '免费', + priceType: item.is_free || item.priceType === 'free' ? 'free' : 'paid', + likes: item.likes || item.likesCount || 0, + participants: item.current_participants || item.currentParticipants || 0, + isLiked: item.is_liked || item.isLiked || false, + isSignedUp: item.is_registered || item.isSignedUp || false, + status: item.status || (item.currentParticipants >= item.maxParticipants && item.maxParticipants > 0 ? 'full' : 'upcoming'), + activityGuideQrcode: item.activityGuideQrcode || item.activity_guide_qrcode || '' + } + }) + + const hasMore = newActivityList.length >= limit && (this.data.activityList.length + newActivityList.length) < total + + if (isLoadMore) { + this.setData({ + activityList: [...this.data.activityList, ...newActivityList], + loadingMore: false, + hasMore, + page: this.data.page + 1, + total + }) + } else { + this.setData({ + activityList: newActivityList, + hasMore, + total, + qrcodeImageUrl: clubQrcode || this.data.qrcodeImageUrl + }) + } + + console.log('[theme-travel] 加载成功,总数:', total, '当前:', this.data.activityList.length, 'hasMore:', hasMore) + } else { + if (isLoadMore) { + this.setData({ loadingMore: false, hasMore: false }) + } else { + this.setData({ activityList: [], hasMore: false }) + } + } + } catch (err) { + console.error('加载活动列表失败', err) + if (isLoadMore) { + this.setData({ loadingMore: false }) + } else { + this.setData({ activityList: [], loading: false }) + } + } finally { + if (!isLoadMore) { + this.setData({ loading: false }) + } + } + }, + + /** + * 标签切换 + */ + onTabChange(e) { + const tab = e.currentTarget.dataset.tab + if (tab === this.data.activeTab) return + + this.setData({ + activeTab: tab, + activityList: [], + page: 1, + hasMore: true + }) + this.loadActivityList() + }, + + /** + * 下拉刷新 + */ + onPullDownRefresh() { + this.loadActivityList(false).finally(() => { + wx.stopPullDownRefresh() + }) + }, + + /** + * 上拉加载更多 + */ + onReachBottom() { + if (this.data.hasMore && !this.data.loadingMore && !this.data.loading) { + this.loadActivityList(true) + } + }, + + /** + * 加载模拟数据(降级方案) + */ + loadMockActivities() { + // 使用空数据,等待后端API返回真实数据 + const mockActivities = [] + + this.setData({ activityList: mockActivities }) + }, + + /** + * 格式化日期 + */ + formatDate(startDate, endDate) { + if (!startDate) return '' + + const formatSingle = (dateStr) => { + const date = new Date(dateStr) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}年${month}月${day}日` + } + + // 如果有结束日期且不同于开始日期,显示日期范围 + if (endDate && endDate !== startDate) { + const startFormatted = formatSingle(startDate) + const endFormatted = formatSingle(endDate) + // 如果是同一年,省略结束日期的年份 + if (startFormatted.split('年')[0] === endFormatted.split('年')[0]) { + return `${startFormatted}-${endFormatted.split('年')[1]}` + } + return `${startFormatted}-${endFormatted}` + } + + return formatSingle(startDate) + }, + + /** + * 立即报名 + */ + async onSignUp(e) { + const id = e.currentTarget.dataset.id + const index = e.currentTarget.dataset.index + + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ url: '/pages/login/login' }) + return + } + + const activity = this.data.activityList[index] + + // 检查活动状态 + if (activity.status === 'full' || activity.status === 'ended') { + const qrCode = activity.activityGuideQrcode || activity.activity_guide_qrcode || this.data.qrcodeImageUrl + this.setData({ + qrcodeImageUrl: qrCode, + showQrcodeModal: true + }) + return + } + + try { + if (activity.isSignedUp) { + // 取消报名 + const res = await api.activity.cancelSignup(id) + if (res.success) { + wx.showToast({ title: '已取消报名', icon: 'success' }) + this.loadActivityList() + } + } else { + // 报名 + const res = await api.activity.signup(id) + if (res.success) { + wx.showToast({ title: '报名成功', icon: 'success' }) + this.loadActivityList() + } else { + // 检查是否需要显示二维码(后端开关关闭或活动已结束) + if (res.code === 'QR_CODE_REQUIRED' || res.error === 'QR_CODE_REQUIRED' || res.code === 'ACTIVITY_ENDED' || res.error === '活动已结束') { + if (activity.activityGuideQrcode || activity.activity_guide_qrcode) { + this.setData({ qrcodeImageUrl: activity.activityGuideQrcode || activity.activity_guide_qrcode }) + } + this.setData({ showQrcodeModal: true }) + if (res.code === 'ACTIVITY_ENDED' || res.error === '活动已结束') { + wx.showToast({ title: '活动已结束,进群查看更多', icon: 'none' }) + } + } else { + wx.showToast({ + title: res.error || '报名失败', + icon: 'none' + }) + } + } + } + } catch (err) { + console.error('报名操作失败', err) + // 捕获特定错误码以显示二维码 + const isQrRequired = err && (err.code === 'QR_CODE_REQUIRED' || (err.data && err.data.code === 'QR_CODE_REQUIRED')) + const isActivityEnded = err && (err.code === 'ACTIVITY_ENDED' || (err.data && err.data.code === 'ACTIVITY_ENDED') || err.error === '活动已结束') + + if (isQrRequired || isActivityEnded) { + if (activity.activityGuideQrcode || activity.activity_guide_qrcode) { + this.setData({ qrcodeImageUrl: activity.activityGuideQrcode || activity.activity_guide_qrcode }) + } + this.setData({ showQrcodeModal: true }) + if (isActivityEnded) { + wx.showToast({ title: '活动已结束,进群查看更多', icon: 'none' }) + } + } else { + wx.showToast({ + title: err.error || err.message || '操作失败', + icon: 'none' + }) + } + } + }, + + /** + * 点赞/取消点赞 + */ + async onLike(e) { + const id = e.currentTarget.dataset.id + const index = e.currentTarget.dataset.index + + if (!app.globalData.isLoggedIn) { + wx.navigateTo({ url: '/pages/login/login' }) + return + } + + try { + const res = await api.activity.toggleLike(id) + if (res.success) { + this.setData({ + [`activityList[${index}].isLiked`]: res.data.isLiked, + [`activityList[${index}].likes`]: res.data.likesCount + }) + } + } catch (err) { + console.error('点赞失败', err) + wx.showToast({ title: '操作失败', icon: 'none' }) + } + }, + + /** + * 点击活动卡片 + */ + onActivityTap(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/activity-detail/activity-detail?id=${id}` + }) + }, + + /** + * 加入高端定制群 + */ + onJoinGroup() { + let qrcodeUrl = this.data.qrcodeImageUrl + + if (!qrcodeUrl && this.data.activityList && this.data.activityList.length > 0) { + const firstWithQrcode = this.data.activityList.find(item => item.activityGuideQrcode || item.activity_guide_qrcode) + if (firstWithQrcode) { + qrcodeUrl = firstWithQrcode.activityGuideQrcode || firstWithQrcode.activity_guide_qrcode + } + } + + if (!qrcodeUrl) { + wx.showToast({ title: '暂无二维码', icon: 'none' }) + return + } + + this.setData({ + showQrcodeModal: true, + qrcodeImageUrl: qrcodeUrl + }) + }, + + /** + * 关闭二维码弹窗 + */ + onCloseQrcodeModal() { + this.setData({ + showQrcodeModal: false + }) + }, + + /** + * 保存二维码 + */ + async onSaveQrcode() { + try { + const { qrcodeImageUrl } = this.data + if (!qrcodeImageUrl) { + wx.showToast({ title: '二维码链接不存在', icon: 'none' }) + return + } + + wx.showLoading({ title: '保存中...' }) + + let filePath = '' + + // 判断是否是 Base64 格式 + if (qrcodeImageUrl.startsWith('data:image')) { + const fs = wx.getFileSystemManager() + const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(qrcodeImageUrl) || [] + if (!format || !bodyData) { + throw new Error('Base64 格式错误') + } + filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.${format}` + fs.writeFileSync(filePath, bodyData, 'base64') + } else { + // 远程 URL 格式 + const downloadRes = await new Promise((resolve, reject) => { + wx.downloadFile({ + url: qrcodeImageUrl, + success: resolve, + fail: reject + }) + }) + + if (downloadRes.statusCode !== 200) { + throw new Error('下载图片失败') + } + filePath = downloadRes.tempFilePath + } + + // 保存到相册 + await new Promise((resolve, reject) => { + wx.saveImageToPhotosAlbum({ + filePath: filePath, + success: resolve, + fail: reject + }) + }) + + wx.hideLoading() + wx.showToast({ title: '保存成功', icon: 'success' }) + this.onCloseQrcodeModal() + } catch (err) { + wx.hideLoading() + console.error('保存二维码失败', err) + + if (err.errMsg && (err.errMsg.includes('auth deny') || err.errMsg.includes('auth denied'))) { + wx.showModal({ + title: '需要授权', + content: '请允许访问相册以保存二维码', + confirmText: '去设置', + success: (res) => { + if (res.confirm) { + wx.openSetting() + } + } + }) + } else { + wx.showToast({ title: err.message || '保存失败', icon: 'none' }) + } + } + }, + + /** + * 阻止冒泡 + */ + preventBubble() { + return + } +}) diff --git a/pages/theme-travel/theme-travel.json b/pages/theme-travel/theme-travel.json new file mode 100644 index 0000000..a3b4779 --- /dev/null +++ b/pages/theme-travel/theme-travel.json @@ -0,0 +1,9 @@ +{ + "navigationStyle": "custom", + "navigationBarTextStyle": "black", + "usingComponents": { + "app-icon": "../../components/icon/icon" + }, + "enablePullDownRefresh": true, + "backgroundTextStyle": "dark" +} diff --git a/pages/theme-travel/theme-travel.wxml b/pages/theme-travel/theme-travel.wxml new file mode 100644 index 0000000..1c9295c --- /dev/null +++ b/pages/theme-travel/theme-travel.wxml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + 高端定制 + + + + + + + + + 高端定制俱乐部 + + 结伴同行 + 精彩旅程 + + + + 点击立即加入 + + + + + + 热门线路 + 共 {{activityList.length}} 条线路 + + + + + + + + + + + + + + {{item.price}} + + + + + {{item.title}} + + + + {{item.date}} + + + + + {{item.location}} · {{item.venue}} + + + + + {{item.heat}} + + + + + + + + {{item.participants}}人已报名 + + + + + + + + + 暂无线路 + + + + 没有更多线路了 ~ + + + + + + + + + + + + + + + 加入高端定制群 + 进群获取更多定制资讯,开启您的精彩旅程 + + + + 长按二维码识别或保存 + + 保存二维码 + + + + diff --git a/pages/theme-travel/theme-travel.wxss b/pages/theme-travel/theme-travel.wxss new file mode 100644 index 0000000..fe5c385 --- /dev/null +++ b/pages/theme-travel/theme-travel.wxss @@ -0,0 +1,607 @@ +/* 主题旅行页面样式 - 优雅蓝紫主题 */ +page { + background: linear-gradient(180deg, #D1C4E9 0%, #E8EAF6 100%); +} + +.page-container { + min-height: 100vh; + background: linear-gradient(180deg, #D1C4E9 0%, #E8EAF6 100%); +} + +/* 固定导航栏容器 */ +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; + background: rgba(248, 249, 252, 0.75); + backdrop-filter: blur(20rpx) saturate(180%); + -webkit-backdrop-filter: blur(20rpx) saturate(180%); +} + +/* 状态栏 */ +.status-bar { + background: transparent; +} + +/* 导航栏 */ +.nav-bar { + display: flex; + align-items: center; + justify-content: center; + padding: 0 32rpx; + background: transparent; + position: relative; +} + +.nav-back { + position: absolute; + left: 32rpx; + top: 50%; + transform: translateY(-50%); + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.back-icon { + width: 48rpx; + height: 48rpx; +} + +.nav-title { + font-size: 36rpx; + font-weight: 700; + color: #1A1A1A; + line-height: 1; +} + +/* 内容滚动区域 */ +.content-scroll { + height: 100vh; + box-sizing: border-box; +} + +/* 推广卡片 - 优雅蓝紫渐变 */ +.city-group-card { + margin: 32rpx; + padding: 32rpx 40rpx; + min-height: 128rpx; + background: linear-gradient(135deg, + rgba(209, 196, 233, 0.6) 0%, + rgba(232, 234, 246, 0.6) 100%); + backdrop-filter: blur(16rpx) saturate(150%); + border: 2rpx solid rgba(103, 58, 183, 0.3); + border-radius: 48rpx; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 4rpx 20rpx rgba(103, 58, 183, 0.12); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.city-group-card:active { + transform: scale(0.98); + box-shadow: 0 2rpx 12rpx rgba(103, 58, 183, 0.18); +} + +.group-info { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8rpx; + padding-right: 24rpx; +} + +.group-title { + font-size: 40rpx; + font-weight: 700; + color: #1A1A1A; + line-height: 1.4; + white-space: nowrap; +} + +.group-tags { + display: flex; + flex-direction: column; + gap: 4rpx; +} + +.tag-item { + font-size: 28rpx; + font-weight: 500; + color: #4A5565; + line-height: 1.4; + white-space: nowrap; +} + +.join-btn { + padding: 0 40rpx; + height: 88rpx; + background: linear-gradient(135deg, #7E57C2 0%, #5E35B1 100%); + border-radius: 100rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + font-weight: 700; + color: #fff; + white-space: nowrap; + flex-shrink: 0; + box-shadow: 0 6rpx 24rpx rgba(103, 58, 183, 0.4), + 0 3rpx 12rpx rgba(103, 58, 183, 0.3); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; +} + +.join-btn:active { + transform: scale(0.96); + box-shadow: 0 4rpx 16rpx rgba(103, 58, 183, 0.45); +} + +/* 活动标签切换 - 横向滚动 */ +.tab-section { + padding: 32rpx 0; + background: transparent; + margin: 0 32rpx 32rpx; + position: relative; + z-index: 1; +} + +.tab-scroll { + width: 100%; + white-space: nowrap; +} + +.tab-scroll::-webkit-scrollbar { + display: none; +} + +.tab-list { + display: inline-flex; + gap: 20rpx; + padding: 0 4rpx; +} + +.tab-item { + padding: 20rpx 48rpx; + border-radius: 100rpx; + font-size: 32rpx; + font-weight: 700; + color: #6A7282; + background: rgba(255, 255, 255, 0.6); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + flex-shrink: 0; + white-space: nowrap; +} + +.tab-item:active { + transform: scale(0.96); +} + +.tab-item.active { + color: #fff; + background: linear-gradient(135deg, #7E57C2 0%, #5E35B1 100%); + box-shadow: 0 12rpx 24rpx rgba(103, 58, 183, 0.3); + transform: scale(1.02); +} + +/* 活动列表标题 */ +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 32rpx; + margin-bottom: 32rpx; +} + +.section-title { + font-size: 44rpx; + font-weight: 700; + color: #1A1A1A; +} + +.activity-count { + font-size: 28rpx; + color: #5E35B1; + font-weight: 500; +} + +/* 活动列表 - 毛玻璃卡片 */ +.activity-list { + padding: 0 32rpx; +} + +.activity-card { + margin-bottom: 32rpx; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(16rpx); + border-radius: 32rpx; + overflow: hidden; + box-shadow: 0 8rpx 32rpx rgba(103, 58, 183, 0.12), + 0 4rpx 16rpx rgba(103, 58, 183, 0.08); + border: 1rpx solid rgba(103, 58, 183, 0.15); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.activity-card:active { + transform: scale(0.98); + box-shadow: 0 4rpx 16rpx rgba(103, 58, 183, 0.15); +} + +/* 活动图片容器 */ +.activity-image-wrap { + position: relative; + width: 100%; + height: 440rpx; + overflow: hidden; + background: linear-gradient(135deg, #E8EAF6 0%, #F3E5F5 100%); +} + +.activity-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.activity-image-gradient { + width: 100%; + height: 100%; + background: linear-gradient(135deg, #E8EAF6 0%, #F3E5F5 100%); +} + +/* 点赞徽章 */ +.like-badge { + position: absolute; + top: 24rpx; + right: 24rpx; + display: flex; + align-items: center; + gap: 8rpx; + padding: 10rpx 20rpx; + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(10rpx); + border-radius: 100rpx; + z-index: 10; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); +} + +.like-icon { + width: 32rpx; + height: 32rpx; +} + +.like-count { + font-size: 24rpx; + color: #4A5565; + font-weight: 600; +} + +.like-badge.liked .like-count { + color: #FF5252; +} + +/* 价格标签 */ +.price-tag { + position: absolute; + bottom: 24rpx; + left: 24rpx; + padding: 10rpx 24rpx; + border-radius: 12rpx; + font-size: 24rpx; + font-weight: 700; + color: #FFFFFF; + z-index: 10; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15); +} + +.price-tag.free { + background: #4CAF50; +} + +.price-tag.paid { + background: #7E57C2; +} + +.location-badge { + position: absolute; + top: 24rpx; + left: 24rpx; + padding: 12rpx 24rpx; + background: rgba(49, 27, 146, 0.85); + backdrop-filter: blur(12rpx); + border-radius: 100rpx; + display: flex; + align-items: center; + gap: 8rpx; + font-size: 24rpx; + color: #FFFFFF; + font-weight: 500; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.15); +} + +.location-icon { + width: 24rpx; + height: 24rpx; +} + +/* 活动信息 */ +.activity-info { + padding: 40rpx; +} + +.activity-title { + font-size: 36rpx; + font-weight: 700; + color: #4527A0; + margin-bottom: 20rpx; + line-height: 1.4; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; +} + +.activity-meta { + display: flex; + align-items: center; + gap: 32rpx; + margin-bottom: 24rpx; +} + +.meta-item { + display: flex; + align-items: center; + gap: 8rpx; + font-size: 26rpx; + color: #5E35B1; +} + +.meta-icon { + width: 28rpx; + height: 28rpx; +} + +.meta-text { + font-size: 26rpx; + color: #4A5565; +} + +.meta-row { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + margin-top: 8rpx; +} + +/* 活动底部 */ +.activity-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 24rpx; + border-top: 1rpx solid rgba(103, 58, 183, 0.1); +} + +.participants { + display: flex; + align-items: center; + gap: 12rpx; +} + +.avatar-stack { + display: flex; + align-items: center; +} + +.mini-avatar { + width: 48rpx; + height: 48rpx; + border-radius: 50%; + background: #E8EAF6; + border: 2rpx solid #fff; + margin-left: -12rpx; +} + +.mini-avatar:first-child { + margin-left: 0; +} + +.participant-text { + font-size: 24rpx; + color: #62748E; +} + +.heat-item { + margin-left: auto; +} + +.heat-text { + color: #FF9800; + font-weight: 600; +} + +/* 立即报名按钮 */ +.signup-btn { + width: 220rpx; + height: 72rpx; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #7E57C2 0%, #5E35B1 100%); + border-radius: 100rpx; + font-size: 28rpx; + font-weight: 700; + color: #FFFFFF; + box-shadow: 0 6rpx 20rpx rgba(103, 58, 183, 0.3); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + white-space: nowrap; + flex-shrink: 0; +} + +.signup-btn:active { + transform: scale(0.95); + box-shadow: 0 4rpx 12rpx rgba(103, 58, 183, 0.35); +} + +/* 空状态 */ +.empty-state { + padding: 120rpx 32rpx; + text-align: center; +} + +.empty-icon { + width: 200rpx; + height: 200rpx; + margin: 0 auto 32rpx; + opacity: 0.5; +} + +.empty-text { + font-size: 28rpx; + color: #9575CD; +} + +/* 二维码弹窗 */ +.qrcode-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + visibility: hidden; + opacity: 0; + transition: all 0.3s ease; +} + +.qrcode-modal.show { + visibility: visible; + opacity: 1; +} + +/* 遮罩层 */ +.modal-mask { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4rpx); +} + +/* 弹窗内容 */ +.modal-content { + position: relative; + width: 680rpx; + background: #FFFFFF; + border-radius: 64rpx; + padding: 64rpx; + box-shadow: 0 50rpx 100rpx -24rpx rgba(0, 0, 0, 0.25); + display: flex; + flex-direction: column; + align-items: center; + z-index: 1; +} + +/* 关闭按钮 */ +.close-btn { + position: absolute; + top: 32rpx; + right: 32rpx; + width: 72rpx; + height: 72rpx; + background: #F1F5F9; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.close-btn:active { + transform: scale(0.9); + background: #E2E8F0; +} + +.close-icon { + width: 40rpx; + height: 40rpx; +} + +/* 标题 */ +.modal-title { + font-size: 48rpx; + font-weight: 700; + color: #1D293D; + text-align: center; + margin-bottom: 16rpx; + line-height: 1.5; +} + +/* 副标题 */ +.modal-subtitle { + font-size: 32rpx; + color: #62748E; + text-align: center; + margin-bottom: 48rpx; + line-height: 1.5; +} + +/* 二维码容器 */ +.qrcode-container { + width: 440rpx; + height: 440rpx; + background: #F8FAFC; + border: 2rpx solid #F1F5F9; + border-radius: 40rpx; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 48rpx; + overflow: hidden; +} + +.qrcode-image { + width: 404rpx; + height: 404rpx; + border-radius: 24rpx; +} + +.modal-tips { + font-size: 24rpx; + color: #999; + margin-bottom: 24rpx; +} + +.save-btn { + width: 552rpx; + height: 116rpx; + background: #07C160; + border-radius: 100rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 40rpx; + font-weight: 700; + color: #FFFFFF; + box-shadow: 0 20rpx 30rpx -6rpx rgba(220, 252, 231, 1), + 0 8rpx 12rpx -8rpx rgba(220, 252, 231, 1); + transition: all 0.3s ease; +} + +.save-btn:active { + transform: scale(0.96); + box-shadow: 0 10rpx 20rpx -6rpx rgba(220, 252, 231, 1); +} diff --git a/pages/webview/webview.js b/pages/webview/webview.js new file mode 100644 index 0000000..88cbe46 --- /dev/null +++ b/pages/webview/webview.js @@ -0,0 +1,34 @@ +// Webview 页面 +Page({ + data: { + webUrl: '', + title: '' + }, + + onLoad(options) { + if (options.url) { + this.setData({ + webUrl: decodeURIComponent(options.url) + }) + } + if (options.title) { + wx.setNavigationBarTitle({ + title: decodeURIComponent(options.title) + }) + this.setData({ title: decodeURIComponent(options.title) }) + } + }, + + goBack() { + wx.navigateBack() + }, + + onShareAppMessage() { + const referralCode = wx.getStorageSync('referralCode') || '' + const referralCodeParam = referralCode ? `&referralCode=${referralCode}` : '' + return { + title: this.data.title || '关于品牌', + path: `/pages/webview/webview?url=${encodeURIComponent(this.data.webUrl)}&title=${encodeURIComponent(this.data.title || '')}${referralCodeParam}` + } + } +}) diff --git a/pages/webview/webview.json b/pages/webview/webview.json new file mode 100644 index 0000000..afde5da --- /dev/null +++ b/pages/webview/webview.json @@ -0,0 +1,4 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "关于品牌" +} diff --git a/pages/webview/webview.wxml b/pages/webview/webview.wxml new file mode 100644 index 0000000..f57149a --- /dev/null +++ b/pages/webview/webview.wxml @@ -0,0 +1,15 @@ + + + + + + + 返回 + + {{title || '加载中...'}} + + + + + + diff --git a/pages/webview/webview.wxss b/pages/webview/webview.wxss new file mode 100644 index 0000000..1756979 --- /dev/null +++ b/pages/webview/webview.wxss @@ -0,0 +1,58 @@ +.page-container { + min-height: 100vh; + background: #f5f5f5; +} + +.unified-header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 88rpx; + padding-top: var(--status-bar-height, 44rpx); + background: linear-gradient(135deg, #B06AB3 0%, #9B59B6 100%); + display: flex; + align-items: center; + justify-content: space-between; + padding-left: 30rpx; + padding-right: 30rpx; + z-index: 1000; +} + +.unified-header-left { + display: flex; + align-items: center; + min-width: 120rpx; +} + +.unified-back-icon { + width: 48rpx; + height: 48rpx; +} + +.unified-back-text { + color: #fff; + font-size: 28rpx; + margin-left: 8rpx; +} + +.unified-header-title { + color: #fff; + font-size: 34rpx; + font-weight: 500; + flex: 1; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.unified-header-right { + min-width: 120rpx; +} + +web-view { + width: 100%; + height: calc(100vh - 88rpx); + margin-top: calc(88rpx + var(--status-bar-height, 44rpx)); +} diff --git a/pages/withdraw-records/withdraw-records.js b/pages/withdraw-records/withdraw-records.js new file mode 100644 index 0000000..ebf9a92 --- /dev/null +++ b/pages/withdraw-records/withdraw-records.js @@ -0,0 +1,186 @@ +// pages/withdraw-records/withdraw-records.js +const api = require('../../utils/api') +const util = require('../../utils/util') + +Page({ + data: { + // 导航栏高度 + statusBarHeight: 44, + navBarHeight: 44, + totalNavHeight: 88, + + // 提现记录列表 + list: [], + + // 分页 + page: 1, + pageSize: 20, + total: 0, + hasMore: true, + + // 筛选状态 + currentStatus: 'all', // all, pending, approved, rejected + statusList: [ + { value: 'all', label: '全部' }, + { value: 'pending', label: '待审核' }, + { value: 'approved', label: '已通过' }, + { value: 'rejected', label: '已拒绝' } + ], + + // 状态 + loading: false, + isEmpty: false + }, + + onLoad(options) { + // 计算导航栏高度 + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 44 + const menuButton = wx.getMenuButtonBoundingClientRect() + const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 + const totalNavHeight = statusBarHeight + navBarHeight + + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight + }) + + // 加载数据 + this.loadWithdrawRecords() + }, + + /** + * 加载提现记录列表 + */ + async loadWithdrawRecords(page = 1) { + if (this.data.loading) return + + this.setData({ loading: true }) + + try { + const params = { + page, + pageSize: this.data.pageSize + } + + // 添加状态筛选 + if (this.data.currentStatus !== 'all') { + params.status = this.data.currentStatus + } + + const res = await api.commission.getWithdrawals(params) + + if (res.success && res.data) { + const dataList = res.data.list || [] + const list = page === 1 ? dataList : [...this.data.list, ...dataList] + const hasMore = dataList.length === this.data.pageSize + const isEmpty = list.length === 0 + + this.setData({ + list, + page, + total: res.data.total || 0, + hasMore, + isEmpty, + loading: false + }) + } else { + throw new Error(res.message || '加载失败') + } + } catch (error) { + console.error('加载提现记录失败:', error) + this.setData({ loading: false }) + wx.showToast({ + title: error.message || '加载失败', + icon: 'none' + }) + } + }, + + /** + * 切换状态筛选 + */ + onStatusChange(e) { + const status = e.currentTarget.dataset.status + if (status === this.data.currentStatus) return + + this.setData({ + currentStatus: status, + page: 1, + list: [], + hasMore: true + }) + + this.loadWithdrawRecords(1) + }, + + /** + * 获取状态文本 + */ + getStatusText(status) { + const statusMap = { + 'pending': '待审核', + 'approved': '已通过', + 'rejected': '已拒绝' + } + return statusMap[status] || status + }, + + /** + * 获取状态样式类 + */ + getStatusClass(status) { + const classMap = { + 'pending': 'status-pending', + 'approved': 'status-approved', + 'rejected': 'status-rejected' + } + return classMap[status] || '' + }, + + /** + * 格式化金额 + */ + formatMoney(amount) { + return util.formatMoney(amount) + }, + + /** + * 格式化时间 + */ + formatTime(timestamp) { + return util.formatDate(timestamp) + }, + + /** + * 下拉刷新 + */ + onPullDownRefresh() { + this.loadWithdrawRecords(1).then(() => { + wx.stopPullDownRefresh() + }) + }, + + /** + * 上拉加载更多 + */ + onReachBottom() { + if (!this.data.hasMore || this.data.loading) return + this.loadWithdrawRecords(this.data.page + 1) + }, + + /** + * 返回上一页 + */ + onBack() { + wx.navigateBack() + }, + + /** + * 重新加载 + */ + onRetry() { + this.loadWithdrawRecords(1) + } +}) diff --git a/pages/withdraw-records/withdraw-records.json b/pages/withdraw-records/withdraw-records.json new file mode 100644 index 0000000..1c9dd73 --- /dev/null +++ b/pages/withdraw-records/withdraw-records.json @@ -0,0 +1,6 @@ +{ + "navigationStyle": "custom", + "enablePullDownRefresh": true, + "backgroundTextStyle": "dark", + "backgroundColor": "#F8F5FF" +} diff --git a/pages/withdraw-records/withdraw-records.wxml b/pages/withdraw-records/withdraw-records.wxml new file mode 100644 index 0000000..47e9b7d --- /dev/null +++ b/pages/withdraw-records/withdraw-records.wxml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + 提现记录 + + + + + + + + + {{item.label}} + + + + + + + + + 单号: {{item.orderNo}} + + {{getStatusText(item.status)}} + + + + + + + 提现金额 + ¥{{formatMoney(item.amount)}} + + + 手续费 + ¥{{formatMoney(item.fee)}} + + + 实际到账 + ¥{{formatMoney(item.actualAmount)}} + + + + + + + 申请时间 + {{formatTime(item.createdAt)}} + + + 处理时间 + {{formatTime(item.processedAt)}} + + + + + + 拒绝原因 + {{item.rejectReason}} + + + + + 💡 + 预计1-3个工作日到账 + + + + + + + + 暂无提现记录 + 完成推广任务后即可申请提现 + + + + + 加载中... + + + + + 没有更多了 + + + + + + diff --git a/pages/withdraw-records/withdraw-records.wxss b/pages/withdraw-records/withdraw-records.wxss new file mode 100644 index 0000000..9d868e8 --- /dev/null +++ b/pages/withdraw-records/withdraw-records.wxss @@ -0,0 +1,303 @@ +/* pages/withdraw-records/withdraw-records.wxss */ + +/* 页面容器 */ +.page { + min-height: 100vh; + background: linear-gradient(180deg, #F8F5FF 0%, #FFFFFF 100%); +} + +/* 固定导航栏容器 */ +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; + background: rgba(242, 237, 255, 0.6); + backdrop-filter: blur(10px); +} + +/* 状态栏 */ +.status-bar { + background: transparent; +} + +/* 导航栏 */ +.nav-bar { + display: flex; + align-items: center; + justify-content: center; + padding: 0 32rpx; + background: transparent; + position: relative; +} + +.nav-back { + position: absolute; + left: 32rpx; + top: 50%; + transform: translateY(-50%); + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.back-icon { + width: 48rpx; + height: 48rpx; +} + +.nav-title { + font-size: 36rpx; + font-weight: 700; + color: #1F2937; + line-height: 1; +} + +/* 内容滚动区域 */ +.content-scroll { + height: 100vh; + box-sizing: border-box; +} + +/* 状态筛选栏 */ +.filter-bar { + display: flex; + align-items: center; + padding: 24rpx 32rpx; + background: #FFFFFF; + margin: 24rpx 32rpx; + border-radius: 16rpx; + box-shadow: 0 4rpx 12rpx rgba(176, 106, 179, 0.08); +} + +.filter-item { + flex: 1; + text-align: center; + padding: 16rpx 0; + font-size: 28rpx; + color: #6B7280; + border-radius: 12rpx; + transition: all 0.3s; +} + +.filter-item.active { + background: linear-gradient(135deg, #B06AB3 0%, #9B4D9E 100%); + color: #FFFFFF; + font-weight: 600; +} + +/* 提现记录列表 */ +.records-list { + padding: 0 32rpx; +} + +.record-card { + background: #FFFFFF; + border-radius: 20rpx; + padding: 32rpx; + margin-bottom: 24rpx; + box-shadow: 0 4rpx 16rpx rgba(176, 106, 179, 0.1); +} + +/* 记录头部 */ +.record-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24rpx; + padding-bottom: 24rpx; + border-bottom: 1rpx solid #F3F4F6; +} + +.order-no { + font-size: 26rpx; + color: #6B7280; +} + +.status { + padding: 8rpx 20rpx; + border-radius: 20rpx; + font-size: 24rpx; + font-weight: 600; +} + +.status-pending { + background: #FEF3C7; + color: #D97706; +} + +.status-approved { + background: #D1FAE5; + color: #059669; +} + +.status-rejected { + background: #FEE2E2; + color: #DC2626; +} + +/* 金额信息 */ +.amount-section { + margin-bottom: 24rpx; +} + +.amount-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16rpx; +} + +.amount-row:last-child { + margin-bottom: 0; +} + +.amount-row.highlight { + padding-top: 16rpx; + border-top: 1rpx dashed #E5E7EB; +} + +.amount-row .label { + font-size: 28rpx; + color: #6B7280; +} + +.amount-row .value { + font-size: 28rpx; + color: #1F2937; + font-weight: 600; +} + +.amount-row .value.primary { + font-size: 36rpx; + color: #B06AB3; + font-weight: 700; +} + +/* 时间信息 */ +.time-section { + padding-top: 24rpx; + border-top: 1rpx solid #F3F4F6; +} + +.time-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12rpx; +} + +.time-row:last-child { + margin-bottom: 0; +} + +.time-row .label { + font-size: 26rpx; + color: #9CA3AF; +} + +.time-row .value { + font-size: 26rpx; + color: #6B7280; +} + +/* 拒绝原因 */ +.reject-reason { + margin-top: 24rpx; + padding: 20rpx; + background: #FEF2F2; + border-radius: 12rpx; + border-left: 4rpx solid #DC2626; +} + +.reason-label { + font-size: 26rpx; + color: #DC2626; + font-weight: 600; + margin-bottom: 8rpx; +} + +.reason-text { + font-size: 26rpx; + color: #991B1B; + line-height: 1.6; +} + +/* 到账说明 */ +.arrival-tip { + margin-top: 24rpx; + padding: 20rpx; + background: #F0FDF4; + border-radius: 12rpx; + display: flex; + align-items: center; +} + +.tip-icon { + font-size: 32rpx; + margin-right: 12rpx; +} + +.tip-text { + font-size: 26rpx; + color: #059669; +} + +/* 空状态 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 120rpx 32rpx; +} + +.empty-icon { + width: 240rpx; + height: 240rpx; + margin-bottom: 32rpx; + opacity: 0.6; +} + +.empty-text { + font-size: 32rpx; + color: #6B7280; + font-weight: 600; + margin-bottom: 16rpx; +} + +.empty-tip { + font-size: 26rpx; + color: #9CA3AF; +} + +/* 加载状态 */ +.loading-more { + text-align: center; + padding: 40rpx 0; +} + +.loading-text { + font-size: 26rpx; + color: #9CA3AF; +} + +/* 没有更多 */ +.no-more { + text-align: center; + padding: 40rpx 0; +} + +.no-more-text { + font-size: 26rpx; + color: #D1D5DB; +} + +/* 底部占位 */ +.bottom-placeholder { + height: 40rpx; +} diff --git a/pages/withdraw/withdraw.js b/pages/withdraw/withdraw.js new file mode 100644 index 0000000..bea067b --- /dev/null +++ b/pages/withdraw/withdraw.js @@ -0,0 +1,132 @@ +const { request } = require('../../utils_new/request'); + +Page({ + data: { + statusBarHeight: 20, + navBarHeight: 44, + totalNavHeight: 64, + balance: '0.00', + amount: '', + withdrawType: 'wechat', + withdrawTypeText: '微信', + submitting: false, + records: [], + withdrawConfig: { + minWithdrawAmount: 1 + } + }, + onLoad() { + const sys = wx.getSystemInfoSync(); + const menu = wx.getMenuButtonBoundingClientRect(); + const statusBarHeight = sys.statusBarHeight || 20; + const navBarHeight = menu.height + (menu.top - statusBarHeight) * 2; + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight: statusBarHeight + navBarHeight + }); + this.load(); + this.fetchConfig(); + this.fetchRecords(); + }, + onBack() { + wx.navigateBack({ delta: 1 }); + }, + async fetchConfig() { + try { + const res = await request({ url: '/api/withdraw/config', method: 'GET' }); + if (res.data && res.data.code === 0 && res.data.data) { + this.setData({ + withdrawConfig: res.data.data + }); + } + } catch (err) { + console.error('Fetch withdraw config failed', err); + } + }, + async fetchRecords() { + try { + const res = await request({ url: '/api/commission?action=withdrawals', method: 'GET' }); + const body = res.data || {}; + if (body.success && body.data) { + const records = body.data.list.map(item => { + // 格式化时间 + const date = new Date(item.createdAt); + item.timeStr = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; + + // 状态映射 + const statusMap = { + 'pending': '待审核', + 'processing': '打款中', + 'completed': '已通过', + 'rejected': '已拒绝' + }; + item.statusText = statusMap[item.status] || '未知'; + return item; + }); + this.setData({ records }); + } + } catch (err) { + console.error('Fetch records failed', err); + } + }, + async load() { + try { + try { + const res = await request({ url: '/api/commission?action=stats', method: 'GET' }); + const body = res.data || {}; + if (!body.success) throw new Error(); + const balance = Number(body.data?.commissionBalance || 0).toFixed(2); + this.setData({ balance }); + } catch (err) { + this.setData({ balance: '888.00' }); // Mock data + } + } catch (e) {} + }, + onAmount(e) { + this.setData({ amount: e.detail.value }); + }, + fillAll() { + this.setData({ amount: this.data.balance }); + }, + async submit() { + if (this.data.submitting) return; + const amountNum = Number(this.data.amount || 0); + const minAmount = this.data.withdrawConfig.minWithdrawAmount || 0; + + if (!amountNum || amountNum <= 0) { + wx.showToast({ title: '请输入正确金额', icon: 'none' }); + return; + } + + if (amountNum < minAmount) { + wx.showToast({ title: `最低提现金额为 ¥${minAmount.toFixed(2)}`, icon: 'none' }); + return; + } + + this.setData({ submitting: true }); + try { + const res = await request({ + url: '/api/commission', + method: 'POST', + data: { + action: 'withdraw', + amount: amountNum, + withdrawType: this.data.withdrawType, + accountInfo: {} + } + }); + const body = res.data || {}; + if (!body.success) throw new Error(body.error || '提交失败'); + wx.showToast({ title: '提交申请成功', icon: 'success' }); + this.setData({ amount: '' }); + this.load(); + this.fetchRecords(); + } catch (e) { + wx.showToast({ title: e.message || '提交失败', icon: 'none' }); + } finally { + this.setData({ submitting: false }); + } + } +}); + diff --git a/pages/withdraw/withdraw.json b/pages/withdraw/withdraw.json new file mode 100644 index 0000000..3153ca5 --- /dev/null +++ b/pages/withdraw/withdraw.json @@ -0,0 +1,5 @@ +{ + "usingComponents": { + "app-icon": "/components/icon/icon" + } +} diff --git a/pages/withdraw/withdraw.wxml b/pages/withdraw/withdraw.wxml new file mode 100644 index 0000000..d450c5e --- /dev/null +++ b/pages/withdraw/withdraw.wxml @@ -0,0 +1,83 @@ + + + + + + 返回 + + 余额提现 + + + + + + + + 可提现佣金 + + + + ¥ + {{balance}} + + + + 最低提现金额 ¥{{withdrawConfig.minWithdrawAmount || '1.00'}} + + + + + + + 提现金额 + + ¥ + + 全部 + + + + + 提现方式 + + + + + + 微信零钱 + + + + + + + + + + + + 提现记录 + + + + + 微信提现 + {{item.timeStr}} + + + -¥{{item.amount}} + {{item.statusText}} + + + + 拒绝原因: + {{item.rejectReason}} + + + + + + + diff --git a/pages/withdraw/withdraw.wxss b/pages/withdraw/withdraw.wxss new file mode 100644 index 0000000..ffd984a --- /dev/null +++ b/pages/withdraw/withdraw.wxss @@ -0,0 +1,302 @@ +.page { + min-height: 100vh; + background: linear-gradient(180deg, #F8F5FF 0%, #FFFFFF 100%); + padding-bottom: env(safe-area-inset-bottom); +} + +/* 顶部导航栏已移除,改用全局 unified-header */ + +.wrap { + padding: 0 32rpx; +} + +.card { + background: #ffffff; + border-radius: 48rpx; + padding: 40rpx; + box-shadow: 0 4rpx 16rpx rgba(176, 106, 179, 0.08); + margin-bottom: 32rpx; + border: 2rpx solid #FDF4F9; +} + +/* Balance Card */ +.balance-card { + background: linear-gradient(135deg, #FFFFFF 0%, #FDF4F9 100%); +} + +.balance-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24rpx; +} + +.label { + font-size: 28rpx; + font-weight: 700; + color: #6B7280; +} + +.balance-row { + display: flex; + align-items: baseline; + gap: 8rpx; + margin-bottom: 32rpx; +} + +.currency { + font-size: 40rpx; + font-weight: 900; + color: #111827; +} + +.value { + font-size: 80rpx; + font-weight: 900; + color: #111827; + line-height: 1; + letter-spacing: -2rpx; +} + +.withdraw-tip { + display: inline-flex; + align-items: center; + gap: 8rpx; + padding: 12rpx 24rpx; + background: rgba(176, 106, 179, 0.1); + border-radius: 999rpx; +} + +.withdraw-tip text { + font-size: 24rpx; + color: #B06AB3; + font-weight: 600; +} + +/* Form Card */ +.field-group { + margin-bottom: 48rpx; +} + +.f-label { + display: block; + font-size: 28rpx; + font-weight: 800; + color: #111827; + margin-bottom: 20rpx; +} + +.input-wrapper { + background: #F9FAFB; + border-radius: 32rpx; + padding: 8rpx 32rpx; + display: flex; + align-items: center; + height: 112rpx; + border: 2rpx solid transparent; + transition: all 0.3s; +} + +.input-wrapper:focus-within { + background: #FFFFFF; + border-color: #B06AB3; + box-shadow: 0 0 0 4rpx rgba(176, 106, 179, 0.1); +} + +.input-prefix { + font-size: 40rpx; + font-weight: 900; + color: #111827; + margin-right: 16rpx; +} + +.input { + flex: 1; + height: 100%; + font-size: 40rpx; + font-weight: 900; + color: #111827; +} + +.placeholder { + color: #D1D5DB; + font-weight: 600; +} + +.all-btn { + font-size: 28rpx; + font-weight: 700; + color: #B06AB3; + padding: 12rpx 24rpx; +} + +.select-wrapper { + background: #F9FAFB; + border-radius: 32rpx; + padding: 24rpx 32rpx; + display: flex; + align-items: center; + justify-content: space-between; + height: 112rpx; +} + +.select-left { + display: flex; + align-items: center; + gap: 16rpx; +} + +.wechat-icon-box { + width: 64rpx; + height: 64rpx; + background: #FFFFFF; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.select-text { + font-size: 30rpx; + font-weight: 700; + color: #111827; +} + +.submit-btn { + width: 100%; + height: 104rpx; + border-radius: 52rpx; + background: linear-gradient(135deg, #B06AB3 0%, #9B4D9E 100%); + color: #ffffff; + font-size: 36rpx; + font-weight: 900; + box-shadow: 0 20rpx 40rpx rgba(176, 106, 179, 0.3); + display: flex; + align-items: center; + justify-content: center; + margin-top: 64rpx; + letter-spacing: 2rpx; + transition: opacity 0.3s; +} + +.submit-btn[disabled] { + opacity: 0.6; + box-shadow: none; +} + +/* Records Section */ +.records-section { + margin-top: 48rpx; + padding-bottom: 64rpx; +} + +.section-header { + margin-bottom: 24rpx; + padding: 0 8rpx; +} + +.section-title { + font-size: 32rpx; + font-weight: 800; + color: #111827; +} + +.record-list { + display: flex; + flex-direction: column; + gap: 24rpx; +} + +.record-item { + background: #ffffff; + border-radius: 32rpx; + padding: 32rpx; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.02); + border: 2rpx solid #F9FAFB; +} + +.record-info { + display: flex; + flex-direction: column; + gap: 8rpx; + flex: 1; +} + +.record-type { + font-size: 28rpx; + font-weight: 700; + color: #111827; +} + +.record-time { + font-size: 24rpx; + color: #9CA3AF; +} + +.record-amount { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 8rpx; +} + +.amount-value { + font-size: 32rpx; + font-weight: 800; + color: #111827; +} + +.record-status { + font-size: 22rpx; + font-weight: 700; + padding: 4rpx 16rpx; + border-radius: 999rpx; +} + +/* Rejection Reason Style */ +.reject-reason { + width: 100%; + margin-top: 20rpx; + padding-top: 20rpx; + border-top: 2rpx dashed #F3F4F6; + display: flex; + flex-direction: row; + align-items: flex-start; +} + +.reason-label { + font-size: 24rpx; + font-weight: 700; + color: #DC2626; + white-space: nowrap; +} + +.reason-content { + font-size: 24rpx; + color: #4B5563; + line-height: 1.4; +} + +.status-pending { + background: #FEF3C7; + color: #D97706; +} + +.status-processing { + background: #DBEAFE; + color: #2563EB; +} + +.status-completed { + background: #D1FAE5; + color: #059669; +} + +.status-rejected { + background: #FEE2E2; + color: #DC2626; +} diff --git a/pages/workbench/workbench.js b/pages/workbench/workbench.js new file mode 100644 index 0000000..65b4877 --- /dev/null +++ b/pages/workbench/workbench.js @@ -0,0 +1,320 @@ +// pages/workbench/workbench.js +const api = require('../../utils/api') +const app = getApp() + +Page({ + data: { + userInfo: {}, + currentStatus: 'offline', + statusText: '离线', + showStatusModal: false, + // 等级信息 + levelCode: 'junior', + levelName: '初级', + textPrice: 0.5, + voicePrice: 1, + stats: { + todayOrders: 0, + todayIncome: '0.00', + totalOrders: 0, + totalIncome: '0.00' + }, + hallOrders: [], + activeOrders: [], + loading: false + }, + + onLoad() { + this.setData({ + userInfo: app.globalData.userInfo || {} + }) + }, + + onShow() { + this.loadCompanionStatus() + this.loadWorkbenchData() + this.loadHallOrders() + this.loadActiveOrders() + }, + + // 加载陪聊师状态(包含等级信息) + async loadCompanionStatus() { + try { + const res = await api.companion.getStatus() + if (res.success && res.data) { + const data = res.data + this.setData({ + currentStatus: data.onlineStatus || 'offline', + statusText: this.getStatusText(data.onlineStatus || 'offline'), + // 等级信息 + levelCode: data.levelCode || 'junior', + levelName: data.levelName || '初级', + textPrice: data.textPrice || 0.5, + voicePrice: data.voicePrice || 1 + }) + } + } catch (err) { + console.error('加载陪聊师状态失败:', err) + } + }, + + // 加载工作台数据 + async loadWorkbenchData() { + try { + const res = await api.companion.getWorkbench() + if (res.success) { + const data = res.data || {} + this.setData({ + stats: { + todayOrders: data.todayOrders || data.todayStats?.orderCount || 0, + todayIncome: (data.todayIncome || data.todayStats?.totalAmount || 0).toFixed(2), + totalOrders: data.totalOrders || data.monthStats?.orderCount || 0, + totalIncome: (data.totalIncome || data.monthStats?.totalAmount || 0).toFixed(2) + } + }) + } + } catch (err) { + console.error('加载工作台数据失败:', err) + } + }, + + // 加载接单大厅订单 + async loadHallOrders() { + try { + const res = await api.order.getHall() + if (res.success) { + const orders = (res.data || []).map(order => ({ + ...order, + createTimeText: this.formatTime(order.created_at), + serviceTypeText: this.getServiceTypeText(order.service_type) + })) + this.setData({ hallOrders: orders }) + } + } catch (err) { + console.error('加载接单大厅失败:', err) + } + }, + + // 加载进行中的订单 + async loadActiveOrders() { + try { + const res = await api.companion.getOrders({ status: 'in_progress' }) + if (res.success) { + const orders = (res.data?.list || []).map(order => ({ + ...order, + remainingTime: this.calculateRemainingTime(order) + })) + this.setData({ activeOrders: orders }) + } + } catch (err) { + console.error('加载进行中订单失败:', err) + } + }, + + // 获取状态文本 + getStatusText(status) { + const statusMap = { + 'online': '在线接单', + 'busy': '忙碌中', + 'offline': '离线' + } + return statusMap[status] || '离线' + }, + + // 获取服务类型文本 + getServiceTypeText(type) { + const typeMap = { + 'chat': '文字聊天', + 'voice': '语音聊天', + 'video': '视频聊天' + } + return typeMap[type] || '聊天服务' + }, + + // 格式化时间 + formatTime(timeStr) { + if (!timeStr) return '' + const date = new Date(timeStr) + const now = new Date() + const diff = now - date + + if (diff < 60000) return '刚刚' + if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前' + if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前' + return `${date.getMonth() + 1}/${date.getDate()}` + }, + + // 计算剩余时间 + calculateRemainingTime(order) { + if (!order.start_time || !order.duration) return '未知' + const startTime = new Date(order.start_time).getTime() + const endTime = startTime + order.duration * 60 * 1000 + const remaining = endTime - Date.now() + + if (remaining <= 0) return '已超时' + const minutes = Math.floor(remaining / 60000) + return `${minutes}分钟` + }, + + // 显示状态选择器 + showStatusPicker() { + this.setData({ showStatusModal: true }) + }, + + // 隐藏状态选择器 + hideStatusPicker() { + this.setData({ showStatusModal: false }) + }, + + // 切换状态 + async changeStatus(e) { + const status = e.currentTarget.dataset.status + if (status === this.data.currentStatus) { + this.hideStatusPicker() + return + } + + wx.showLoading({ title: '切换中...' }) + try { + const res = await api.companion.updateStatus(status) + if (res.success) { + this.setData({ + currentStatus: status, + statusText: this.getStatusText(status) + }) + wx.showToast({ title: '状态已更新', icon: 'success' }) + } else { + wx.showToast({ title: res.message || '切换失败', icon: 'none' }) + } + } catch (err) { + wx.showToast({ title: '切换失败', icon: 'none' }) + } finally { + wx.hideLoading() + this.hideStatusPicker() + } + }, + + // 刷新接单大厅 + refreshHall() { + wx.showLoading({ title: '刷新中...' }) + this.loadHallOrders().finally(() => { + wx.hideLoading() + wx.showToast({ title: '已刷新', icon: 'success' }) + }) + }, + + // 接受订单 + async acceptOrder(e) { + const orderId = e.currentTarget.dataset.id + + wx.showModal({ + title: '确认接单', + content: '确定要接受这个订单吗?', + success: async (res) => { + if (res.confirm) { + wx.showLoading({ title: '处理中...' }) + try { + const result = await api.order.accept(orderId) + if (result.success) { + wx.showToast({ title: '接单成功', icon: 'success' }) + this.loadHallOrders() + this.loadActiveOrders() + this.loadWorkbenchData() + } else { + wx.showToast({ title: result.message || '接单失败', icon: 'none' }) + } + } catch (err) { + wx.showToast({ title: '接单失败', icon: 'none' }) + } finally { + wx.hideLoading() + } + } + } + }) + }, + + // 拒绝订单 + async rejectOrder(e) { + const orderId = e.currentTarget.dataset.id + + wx.showModal({ + title: '拒绝订单', + content: '确定要拒绝这个订单吗?', + success: async (res) => { + if (res.confirm) { + wx.showLoading({ title: '处理中...' }) + try { + const result = await api.order.reject(orderId, '陪聊师暂时无法接单') + if (result.success) { + wx.showToast({ title: '已拒绝', icon: 'success' }) + this.loadHallOrders() + } else { + wx.showToast({ title: result.message || '操作失败', icon: 'none' }) + } + } catch (err) { + wx.showToast({ title: '操作失败', icon: 'none' }) + } finally { + wx.hideLoading() + } + } + } + }) + }, + + // 结束服务 + async endService(e) { + const orderId = e.currentTarget.dataset.id + + wx.showModal({ + title: '结束服务', + content: '确定要结束这个服务吗?', + success: async (res) => { + if (res.confirm) { + wx.showLoading({ title: '处理中...' }) + try { + const result = await api.order.endService(orderId) + if (result.success) { + wx.showToast({ title: '服务已结束', icon: 'success' }) + this.loadActiveOrders() + this.loadWorkbenchData() + } else { + wx.showToast({ title: result.message || '操作失败', icon: 'none' }) + } + } catch (err) { + wx.showToast({ title: '操作失败', icon: 'none' }) + } finally { + wx.hideLoading() + } + } + } + }) + }, + + // 跳转到聊天 + goToChat(e) { + const order = e.currentTarget.dataset.order + wx.navigateTo({ + url: `/pages/companion-chat/companion-chat?orderId=${order.id}&userId=${order.user_id}` + }) + }, + + // 跳转到订单列表 + goToOrders() { + wx.navigateTo({ url: '/pages/companion-orders/companion-orders' }) + }, + + // 跳转到客户管理 + goToCustomers() { + wx.navigateTo({ url: '/pages/customer-management/customer-management' }) + }, + + // 跳转到提现 + goToWithdraw() { + wx.navigateTo({ url: '/pages/withdraw/withdraw' }) + }, + + // 跳转到佣金明细 + goToCommission() { + wx.navigateTo({ url: '/pages/commission/commission' }) + } +}) diff --git a/pages/workbench/workbench.json b/pages/workbench/workbench.json new file mode 100644 index 0000000..9740c19 --- /dev/null +++ b/pages/workbench/workbench.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "工作台", + "navigationBarBackgroundColor": "#E8C3D4", + "usingComponents": {} +} diff --git a/pages/workbench/workbench.wxml b/pages/workbench/workbench.wxml new file mode 100644 index 0000000..b5d9ad3 --- /dev/null +++ b/pages/workbench/workbench.wxml @@ -0,0 +1,175 @@ + + + + var DEFAULT_AVATAR = 'https://ai-c.maimanji.com/images/default-avatar.png'; + module.exports = { + DEFAULT_AVATAR: DEFAULT_AVATAR, + getAvatar: function(avatar) { + return avatar || DEFAULT_AVATAR; + } + }; + + + + + + + + {{userInfo.nickname || '陪聊师'}} + + {{statusText}} + + + + + 切换状态 + + + + + + + + + {{levelName}} + + 当前等级 + + + + 文字服务 + ¥{{textPrice}}/分钟 + + + + 语音服务 + ¥{{voicePrice}}/分钟 + + + + + + + + + {{stats.todayOrders || 0}} + 今日订单 + + + ¥{{stats.todayIncome || '0.00'}} + 今日收入 + + + + + {{stats.totalOrders || 0}} + 总订单数 + + + ¥{{stats.totalIncome || '0.00'}} + 总收入 + + + + + + + + + 我的订单 + + + + 客户管理 + + + + 提现 + + + + 佣金明细 + + + + + + + 接单大厅 + 刷新 + + + + + + + + + + {{item.serviceTypeText}} + {{item.duration}}分钟 + ¥{{item.amount}} + + + {{item.message}} + + + + + + + + + + + 暂无待接订单 + + + + + + + 进行中的订单 + + + + + + + + + + + + + + + + + + + + + 选择状态 + + + + + 在线接单 + + + + 忙碌中 + + + + 离线 + + + diff --git a/pages/workbench/workbench.wxss b/pages/workbench/workbench.wxss new file mode 100644 index 0000000..9f8d632 --- /dev/null +++ b/pages/workbench/workbench.wxss @@ -0,0 +1,509 @@ +/* pages/workbench/workbench.wxss */ +.container { + min-height: 100vh; + background: linear-gradient(180deg, #E8C3D4 0%, #F5E6EC 100%); + padding: 20rpx; + padding-bottom: 40rpx; +} + +/* 状态栏 */ +.status-bar { + display: flex; + justify-content: space-between; + align-items: center; + background: #fff; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 20rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.status-info { + display: flex; + align-items: center; +} + +.avatar { + width: 100rpx; + height: 100rpx; + border-radius: 50%; + margin-right: 20rpx; +} + +.status-text { + display: flex; + flex-direction: column; +} + +.name { + font-size: 32rpx; + font-weight: 600; + color: #333; + margin-bottom: 8rpx; +} + +.status-tag { + display: inline-flex; + align-items: center; + padding: 6rpx 16rpx; + border-radius: 20rpx; + font-size: 24rpx; +} + +.status-tag.online { + background: #e8f5e9; + color: #4caf50; +} + +.status-tag.busy { + background: #fff3e0; + color: #ff9800; +} + +.status-tag.offline { + background: #f5f5f5; + color: #9e9e9e; +} + +.status-switch { + display: flex; + align-items: center; + color: #b06ab3; + font-size: 28rpx; +} + +.status-switch .arrow { + width: 24rpx; + height: 24rpx; + margin-left: 8rpx; +} + +/* 数据统计卡片 */ +.stats-card { + background: linear-gradient(135deg, #b06ab3 0%, #d4a5d6 100%); + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 20rpx; + box-shadow: 0 4rpx 20rpx rgba(176, 106, 179, 0.3); +} + +.stats-row { + display: flex; + justify-content: space-around; + margin-bottom: 20rpx; +} + +.stats-row:last-child { + margin-bottom: 0; +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; +} + +.stat-value { + font-size: 40rpx; + font-weight: 700; + color: #fff; + margin-bottom: 8rpx; +} + +.stat-label { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.8); +} + +/* 快捷操作 */ +.quick-actions { + display: flex; + justify-content: space-around; + background: #fff; + border-radius: 20rpx; + padding: 30rpx 20rpx; + margin-bottom: 20rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.action-item { + display: flex; + flex-direction: column; + align-items: center; +} + +.action-item image { + width: 60rpx; + height: 60rpx; + margin-bottom: 12rpx; +} + +.action-item text { + font-size: 24rpx; + color: #666; +} + +/* 接单大厅 */ +.order-hall { + background: #fff; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 20rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20rpx; +} + +.section-title { + font-size: 32rpx; + font-weight: 600; + color: #333; +} + +.section-more { + font-size: 26rpx; + color: #b06ab3; +} + +.hall-list { + display: flex; + flex-direction: column; + gap: 20rpx; +} + +.hall-item { + background: #fafafa; + border-radius: 16rpx; + padding: 24rpx; +} + +.hall-user { + display: flex; + align-items: center; + margin-bottom: 16rpx; +} + +.user-avatar { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + margin-right: 16rpx; +} + +.user-info { + display: flex; + flex-direction: column; +} + +.user-name { + font-size: 28rpx; + font-weight: 500; + color: #333; +} + +.order-time { + font-size: 24rpx; + color: #999; + margin-top: 4rpx; +} + +.hall-content { + display: flex; + align-items: center; + gap: 16rpx; + margin-bottom: 12rpx; +} + +.service-type { + background: #e8c3d4; + color: #b06ab3; + padding: 6rpx 16rpx; + border-radius: 8rpx; + font-size: 24rpx; +} + +.service-duration { + font-size: 26rpx; + color: #666; +} + +.service-price { + font-size: 32rpx; + font-weight: 600; + color: #b06ab3; + margin-left: auto; +} + +.hall-message { + background: #fff; + padding: 16rpx; + border-radius: 8rpx; + margin-bottom: 16rpx; +} + +.hall-message text { + font-size: 26rpx; + color: #666; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.hall-actions { + display: flex; + justify-content: flex-end; + gap: 20rpx; +} + +.btn-reject { + background: #f5f5f5; + color: #666; + font-size: 26rpx; + padding: 12rpx 32rpx; + border-radius: 30rpx; + border: none; +} + +.btn-accept { + background: linear-gradient(135deg, #b06ab3 0%, #d4a5d6 100%); + color: #fff; + font-size: 26rpx; + padding: 12rpx 32rpx; + border-radius: 30rpx; + border: none; +} + +.empty-hall { + display: flex; + flex-direction: column; + align-items: center; + padding: 60rpx 0; +} + +.empty-hall image { + width: 200rpx; + height: 200rpx; + margin-bottom: 20rpx; + opacity: 0.5; +} + +.empty-hall text { + font-size: 28rpx; + color: #999; +} + +/* 进行中的订单 */ +.active-orders { + background: #fff; + border-radius: 20rpx; + padding: 30rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.active-list { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.active-item { + display: flex; + justify-content: space-between; + align-items: center; + background: #fafafa; + border-radius: 16rpx; + padding: 20rpx; +} + +.active-user { + display: flex; + align-items: center; +} + +.remaining-time { + font-size: 24rpx; + color: #ff9800; + margin-top: 4rpx; +} + +.btn-end { + background: #ff5722; + color: #fff; + font-size: 24rpx; + padding: 10rpx 24rpx; + border-radius: 30rpx; + border: none; +} + +/* 状态选择弹窗 */ +.status-picker-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 100; +} + +.status-picker { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: #fff; + border-radius: 30rpx 30rpx 0 0; + padding: 30rpx; + z-index: 101; + animation: slideUp 0.3s ease; +} + +@keyframes slideUp { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} + +.picker-header { + text-align: center; + padding-bottom: 30rpx; + border-bottom: 1rpx solid #eee; +} + +.picker-header text { + font-size: 32rpx; + font-weight: 600; + color: #333; +} + +.picker-options { + padding: 20rpx 0; +} + +.picker-option { + display: flex; + align-items: center; + padding: 30rpx 20rpx; + border-radius: 12rpx; +} + +.picker-option.active { + background: #f5e6ec; +} + +.status-dot { + width: 20rpx; + height: 20rpx; + border-radius: 50%; + margin-right: 20rpx; +} + +.status-dot.online { + background: #4caf50; +} + +.status-dot.busy { + background: #ff9800; +} + +.status-dot.offline { + background: #9e9e9e; +} + +.picker-option text { + font-size: 30rpx; + color: #333; +} + + +/* 等级信息卡片 */ +.level-card { + background: #fff; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 20rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.level-header { + display: flex; + align-items: center; + margin-bottom: 24rpx; +} + +.level-badge { + padding: 8rpx 24rpx; + border-radius: 20rpx; + margin-right: 16rpx; +} + +.level-badge.level-junior { + background: linear-gradient(135deg, #a8d8ea 0%, #6bb3d9 100%); +} + +.level-badge.level-intermediate { + background: linear-gradient(135deg, #b8e986 0%, #7bc96f 100%); +} + +.level-badge.level-senior { + background: linear-gradient(135deg, #ffd700 0%, #ffb347 100%); +} + +.level-badge.level-expert { + background: linear-gradient(135deg, #e8b4d8 0%, #c984cd 100%); +} + +.level-text { + font-size: 28rpx; + font-weight: 600; + color: #fff; +} + +.level-title { + font-size: 28rpx; + color: #666; +} + +.level-prices { + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #fce7f3 0%, #f3e8ff 100%); + border-radius: 16rpx; + padding: 24rpx; +} + +.level-prices .price-item { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; +} + +.level-prices .price-label { + font-size: 24rpx; + color: #6a7282; + margin-bottom: 8rpx; +} + +.level-prices .price-value { + font-size: 32rpx; + font-weight: 700; + color: #e91e63; +} + +.level-prices .price-divider { + width: 2rpx; + height: 60rpx; + background: rgba(0, 0, 0, 0.1); +} diff --git a/project.config.json b/project.config.json new file mode 100644 index 0000000..94c7716 --- /dev/null +++ b/project.config.json @@ -0,0 +1,58 @@ +{ + "description": "中文版手机交友App - 微信小程序", + "packOptions": { + "ignore": [], + "include": [] + }, + "setting": { + "bundle": false, + "userConfirmedBundleSwitch": false, + "urlCheck": true, + "scopeDataCheck": false, + "coverView": true, + "es6": true, + "postcss": true, + "compileHotReLoad": false, + "lazyloadPlaceholderEnable": false, + "preloadBackgroundData": false, + "minified": true, + "autoAudits": false, + "newFeature": false, + "uglifyFileName": false, + "uploadWithSourceMap": true, + "useIsolateContext": true, + "nodeModules": false, + "enhance": true, + "useMultiFrameRuntime": true, + "useApiHook": true, + "useApiHostProcess": true, + "showShadowRootInWxmlPanel": true, + "packNpmManually": false, + "packNpmRelationList": [], + "minifyWXSS": true, + "showES6CompileOption": false, + "minifyWXML": true, + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + }, + "compileWorklet": false, + "localPlugins": false, + "disableUseStrict": false, + "useCompilerPlugins": false, + "condition": false, + "swc": false, + "disableSWC": true + }, + "compileType": "miniprogram", + "libVersion": "3.13.1", + "appid": "wx02babe43d1ef4434", + "projectname": "dating-miniprogram", + "condition": {}, + "editorSetting": { + "tabIndent": "insertSpaces", + "tabSize": 2 + }, + "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..264cc82 --- /dev/null +++ b/project.private.config.json @@ -0,0 +1,24 @@ +{ + "libVersion": "3.13.1", + "projectname": "%E5%BF%83%E4%BC%B4", + "condition": {}, + "setting": { + "urlCheck": false, + "coverView": true, + "lazyloadPlaceholderEnable": false, + "skylineRenderEnable": false, + "preloadBackgroundData": false, + "autoAudits": false, + "useApiHook": true, + "useApiHostProcess": true, + "showShadowRootInWxmlPanel": true, + "useStaticServer": false, + "useLanDebug": false, + "showES6CompileOption": false, + "compileHotReLoad": true, + "checkInvalidKey": true, + "ignoreDevUnusedFiles": true, + "bigPackageSizeSupport": false, + "useIsolateContext": true + } +} \ No newline at end of file diff --git a/sitemap.json b/sitemap.json new file mode 100644 index 0000000..55d1d29 --- /dev/null +++ b/sitemap.json @@ -0,0 +1,7 @@ +{ + "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", + "rules": [{ + "action": "allow", + "page": "*" + }] +} diff --git a/subpackages/cooperation/images/icon_back.png b/subpackages/cooperation/images/icon_back.png new file mode 100644 index 0000000000000000000000000000000000000000..1cc49dd6670095c2687193bb3f3d1c813f68126c GIT binary patch literal 490 zcmVVLM2uL#Z+p(UFag9HAp4N8kqZ280bpMn;a%8^Fj7dV~%gBGjQQ7@L6wC1mik zt;(MiwbJYID{7SI2ar-qDW#NxLI_}Tc5z!FE&;IxqE)+DH=yJQoK0r)JC1wcuQgvY zz80gC({{hxzJr1a9R0)rG=CO|mY~Efurm&%k6KXT64)0f5OD=c%pWrU#jzKD$lQPu z(?IJuVHW5SCrkpp;zaR4&p1&u&^t~P3ycsaiUdZ96Y7DH;)H5ov^b#_7%@($y^B^v z*FDQFaYFB98?o+LC2*p);pI)Ld|Y)bUyjDpbKbP5#-Tq0FR z7N5KATeL{^nM}0EB15N0k;SHJQ6h_u4I@MrpNo1&nhbtDBh4n;UXiAwdyh!-vxz~G zif0ophc5#n=N$M13f#^nJ~r$7FN3oQ*WR&1HMO8r~c~ gLP{y6lu|l$-n+a07*qoM6N<$f_pT?R{#J2 literal 0 HcmV?d00001 diff --git a/subpackages/cooperation/images/icon_chevron_gray.png b/subpackages/cooperation/images/icon_chevron_gray.png new file mode 100644 index 0000000000000000000000000000000000000000..8f179703ee8214b3015aee36048e18cb4603d1f3 GIT binary patch literal 394 zcmV;50d@X~P)S6UggKJ ziJWupzA*{poYx^f?XX{_uRf(TkdZ(FA$B|B`i6~H-2uU5L=^ZR6K^adMS*2n_O(^U2$qp5fnXJ>76=xRs)3*# z**s8RO6hs1Q`laPleBu=lYn~Q+3bAmJb>idlFFoNUp?9 z@fDAr1QdrE{Wwq>sBPjv;a6uD2aS^j+c;>LZrR6ycujJPI9QUvc}-#;ce)levWx@K o9mJX>Jp3Jui{2{EIp-YF4^FJ&Yg@`0yZ`_I07*qoM6N<$g0JwMVgLXD literal 0 HcmV?d00001 diff --git a/subpackages/cooperation/images/icon_chevron_white.png b/subpackages/cooperation/images/icon_chevron_white.png new file mode 100644 index 0000000000000000000000000000000000000000..15a5278c0510376f2fc75ecc4f350af2f363f48a GIT binary patch literal 363 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 literal 0 HcmV?d00001 diff --git a/subpackages/cooperation/images/icon_entertainment.png b/subpackages/cooperation/images/icon_entertainment.png new file mode 100644 index 0000000000000000000000000000000000000000..e24d35224c446c26ba0f35037f0317654ffb6985 GIT binary patch literal 2490 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 literal 0 HcmV?d00001 diff --git a/subpackages/cooperation/pages/cooperation/cooperation.js b/subpackages/cooperation/pages/cooperation/cooperation.js new file mode 100644 index 0000000..43cb307 --- /dev/null +++ b/subpackages/cooperation/pages/cooperation/cooperation.js @@ -0,0 +1,182 @@ +const api = require('../../../../utils/api') + +Page({ + data: { + statusBarHeight: 20, + navBarHeight: 44, + totalNavHeight: 64, + swiperHeight: 400, + sliderIndex: 0, + sliderList: [ + + { id: 'listening', src: 'https://ai-c.maimanji.com/images/cooperation/slider_listening.png' }, + { id: 'eldercare', src: 'https://ai-c.maimanji.com/images/cooperation/slider_eldercare.png' }, + { id: 'highend', src: 'https://ai-c.maimanji.com/images/cooperation/slider_highend.png' } + ], + entryList: [ + { + id: 'entertainment', + title: '休闲娱乐', + subTitle: '精选活动 精彩人生', + icon: '/subpackages/cooperation/images/icon_entertainment.png' + }, + { + id: 'eldercare', + title: '智慧养老', + subTitle: '科技赋能 安享晚年', + icon: '/subpackages/cooperation/images/icon_eldercare.png' + } + ] + }, + + onLoad() { + const sys = wx.getSystemInfoSync(); + const menu = wx.getMenuButtonBoundingClientRect(); + const statusBarHeight = sys.statusBarHeight || 20; + const navBarHeight = menu.height + (menu.top - statusBarHeight) * 2; + this.setData({ + statusBarHeight, + navBarHeight, + totalNavHeight: statusBarHeight + navBarHeight + }); + this.loadBanners(); + this.loadEntries(); + }, + + /** + * 处理图片URL,如果是相对路径则拼接域名,并设置清晰度为85 + */ + processImageUrl(url) { + if (!url) return '' + let fullUrl = url + if (!url.startsWith('http://') && !url.startsWith('https://')) { + const baseUrl = 'https://ai-c.maimanji.com' + fullUrl = baseUrl + (url.startsWith('/') ? '' : '/') + url + } + + // 添加清晰度参数 q=85 + if (fullUrl.includes('?')) { + if (!fullUrl.includes('q=')) { + fullUrl += '&q=85' + } + } else { + fullUrl += '?q=85' + } + return fullUrl + }, + + /** + * 轮播图图片加载完成,自适应高度 + */ + onBannerLoad(e) { + if (this.data.swiperHeight !== 400) return; // 只计算一次 + const { width, height } = e.detail; + const sysInfo = wx.getSystemInfoSync(); + // 减去左右padding (24rpx * 2) - 根据 cooperation.wxss 的 .slider-card + const swiperWidth = sysInfo.windowWidth - (24 * 2 / 750 * sysInfo.windowWidth); + const ratio = width / height; + const swiperHeight = swiperWidth / ratio; + const swiperHeightRpx = swiperHeight * (750 / sysInfo.windowWidth); + + this.setData({ + swiperHeight: swiperHeightRpx + }); + }, + + /** + * 加载合作页Banner + */ + async loadBanners() { + try { + const res = await api.pageAssets.getCooperationBanners() + console.log('合作页Banner API响应:', res) + + if (res.success && res.data && res.data.length > 0) { + const sliderList = res.data.map(item => ({ + id: item.id, + src: this.processImageUrl(item.asset_url), + link: item.description // 假设描述字段存放跳转链接 + })) + this.setData({ sliderList }) + } + } catch (err) { + console.error('加载合作页Banner失败', err) + } + }, + + /** + * 加载合作页入口图标 + */ + async loadEntries() { + try { + // 合作页的入口图标与服务页的服务图标共用 service_icons 分组 + const res = await api.pageAssets.getAssets('service_icons') + console.log('合作页入口图标 API响应:', res) + + if (res.success && res.data) { + const icons = res.data + const { entryList } = this.data + + const updatedEntryList = entryList.map(item => { + // 尝试匹配图标,例如 id='medical' 对应 icons['medical'] + const iconUrl = icons[item.id] + if (iconUrl) { + return { + ...item, + icon: this.processImageUrl(iconUrl) + } + } + return item + }) + + this.setData({ entryList: updatedEntryList }) + } + } catch (err) { + console.error('加载合作页入口图标失败', err) + } + }, + + onBack() { + wx.navigateBack({ delta: 1 }); + }, + + onSwiperChange(e) { + this.setData({ sliderIndex: Number(e.detail.current || 0) }); + }, + + onTapSwiper(e) { + const index = e.currentTarget.dataset.index + const item = this.data.sliderList[index] + if (item && item.link) { + wx.navigateTo({ url: item.link }) + } + }, + + onTapEntry(e) { + const id = e.currentTarget.dataset.id; + // Find link from data + const entry = this.data.entryList.find(x => x.id === id); + if (entry && entry.link) { + wx.navigateTo({ url: entry.link }); + return; + } + + // Fallback logic if link missing but id matches known routes + const routes = { + entertainment: '/pages/entertainment-apply/entertainment-apply', + medical: '/pages/medical-apply/medical-apply', + housekeeping: '/pages/housekeeping-apply/housekeeping-apply', + eldercare: '/pages/eldercare-apply/eldercare-apply' + }; + const url = routes[id]; + if (url) { + wx.navigateTo({ url }); + } else { + wx.showToast({ title: '敬请期待', icon: 'none' }); + } + }, + + onTapOrders() { + wx.navigateTo({ url: '/pages/orders/orders' }); + } +}); diff --git a/subpackages/cooperation/pages/cooperation/cooperation.json b/subpackages/cooperation/pages/cooperation/cooperation.json new file mode 100644 index 0000000..3153ca5 --- /dev/null +++ b/subpackages/cooperation/pages/cooperation/cooperation.json @@ -0,0 +1,5 @@ +{ + "usingComponents": { + "app-icon": "/components/icon/icon" + } +} diff --git a/subpackages/cooperation/pages/cooperation/cooperation.wxml b/subpackages/cooperation/pages/cooperation/cooperation.wxml new file mode 100644 index 0000000..c1da704 --- /dev/null +++ b/subpackages/cooperation/pages/cooperation/cooperation.wxml @@ -0,0 +1,60 @@ + + + + + + 返回 + + 合作入驻 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/subpackages/cooperation/pages/cooperation/cooperation.wxss b/subpackages/cooperation/pages/cooperation/cooperation.wxss new file mode 100644 index 0000000..3cc9862 --- /dev/null +++ b/subpackages/cooperation/pages/cooperation/cooperation.wxss @@ -0,0 +1,195 @@ +.page { + min-height: 100vh; + background: #F5F7FA; + padding-bottom: env(safe-area-inset-bottom); +} + +/* 顶部导航栏已移除,改用全局 unified-header */ + +.container { + padding: 32rpx; + width: 100%; +} + +.content { + flex: 1; +} + +.slider-card { + width: 100%; + height: 342rpx; + border-radius: 32rpx; + overflow: hidden; + margin: 0 0 48rpx; + box-shadow: 0 2rpx 4rpx -2rpx rgba(0, 0, 0, 0.1), 0 2rpx 6rpx 0 rgba(0, 0, 0, 0.1); + position: relative; +} + +.slider { + width: 100%; + height: 100%; +} + +.slide { + width: 100%; + height: 100%; + position: relative; +} + +.slide-image { + width: 100%; + height: 329rpx; + margin-top: 0; +} + +.slide-mask { + position: absolute; + left: 0; + right: 0; + bottom: 13rpx; + height: 329rpx; + background: linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0) 100%); +} + +.dots { + position: absolute; + left: 0; + right: 0; + bottom: 45rpx; + display: flex; + justify-content: center; + pointer-events: none; +} + +.dot-item { + width: 40rpx; + height: 40rpx; + display: flex; + align-items: center; + justify-content: center; + margin: 0 10rpx; +} + +.dot { + width: 16rpx; + height: 16rpx; + border-radius: 999rpx; + background: rgba(255, 255, 255, 0.4); +} + +.dot-active { + background: rgba(255, 255, 255, 1); +} + +.entry-list { + display: flex; + flex-direction: column; + margin-bottom: 48rpx; + width: 100%; + align-items: stretch; +} + +.entry-btn { + width: 100%; + min-width: 100%; + height: 196rpx; + border-radius: 32rpx; + background: #FFFFFF; + box-shadow: 0 2rpx 4rpx -2rpx rgba(0, 0, 0, 0.1), 0 2rpx 6rpx 0 rgba(0, 0, 0, 0.1); + padding: 0 40rpx; + display: flex; + align-items: center; + justify-content: flex-start; + text-align: left; + box-sizing: border-box; + align-self: stretch; +} + +.entry-icon { + width: 112rpx; + height: 112rpx; + margin-right: 32rpx; +} + +.entry-texts { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + margin-right: 32rpx; +} + +.entry-title { + display: block; + font-size: 36rpx; + font-weight: 900; + color: #101828; + line-height: 56rpx; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.entry-sub { + display: block; + font-size: 28rpx; + font-weight: 400; + color: #6A7282; + line-height: 40rpx; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.entry-chevron { + width: 40rpx; + height: 40rpx; + margin-left: auto; +} + +.orders-btn { + width: 100%; + min-width: 100%; + align-self: stretch; + box-sizing: border-box; + height: 196rpx; + border-radius: 32rpx; + background: linear-gradient(180deg, rgba(176, 106, 179, 1) 0%, rgba(156, 90, 157, 1) 100%); + box-shadow: 0 8rpx 12rpx -8rpx rgba(0, 0, 0, 0.1), 0 20rpx 30rpx -6rpx rgba(0, 0, 0, 0.1); + padding: 0 40rpx; + display: flex; + align-items: center; + justify-content: flex-start; + text-align: left; + margin-bottom: 48rpx; +} + +.orders-icon { + width: 112rpx; + height: 112rpx; + margin-right: 32rpx; +} + +.orders-title { + font-size: 36rpx; + font-weight: 900; + color: #FFFFFF; + line-height: 56rpx; +} + +.orders-sub { + font-size: 28rpx; + font-weight: 400; + color: rgba(255, 255, 255, 0.8); + line-height: 40rpx; +} + +.orders-chevron { + width: 48rpx; + height: 48rpx; + margin-left: auto; +} + +.entry-btn + .entry-btn { + margin-top: 32rpx; +} diff --git a/team_page_redesign.md b/team_page_redesign.md new file mode 100644 index 0000000..9c2e01a --- /dev/null +++ b/team_page_redesign.md @@ -0,0 +1,48 @@ +# Team Page Redesign & Integration + +## Overview +The "My Team" page (`pages/team`) has been redesigned to match the new Figma specifications, including a dynamic "Guardian Member" card and a modernized member list. The data integration has been updated to use real backend APIs for team statistics and referral lists. + +## Changes + +### 1. UI Redesign (WXML & WXSS) +- **Guardian Card**: Added a visual card at the top displaying the user's membership level and team stats. + - Dynamic Title: Shows "Guardian Member", "VIP Member", etc. based on `cardType`. + - Stats: Displays "Direct Referrals" and "Team Total". +- **Member List**: + - New card style for each member. + - Shows Avatar, Name, Join Date, "First Referral" tag, and Contribution Amount. + - Handles empty states and loading states. +- **Styling**: + - Implemented purple gradient themes. + - Used `app-icon` for consistent iconography. + - Refined spacing and typography. + +### 2. Logic & Integration (JS) +- **Data Fetching**: + - `api.commission.getStats`: Fetches team statistics (`todayReferrals`, `totalReferrals`, `totalContribution`, `cardType`). + - `api.commission.getReferrals` (via direct request): Fetches the list of direct referrals. + - **Robust Mapping**: Added logic to handle potential API field variations (e.g., `user.nickname` vs `userName`, `snake_case` vs `camelCase`). +- **Navigation**: + - Added `goDetail` to navigate to `pages/commission/commission` for detailed performance breakdowns. +- **Dynamic Content**: + - `cardTitle`: Computed from `cardType` (e.g., 'guardian_card' -> '守护会员'). + +### 3. Profile Page Integration +- Updated "Customer Management" section in `pages/profile`. +- Replaced old cards with "My Team" and "Performance Data". +- "Performance Data" displays the `totalContribution` fetched from commission stats. +- Both cards navigate to the Team page for consistency. + +## API Consistency +- **Team Stats**: Matches `/api/commission?action=stats`. +- **Referral List**: Matches `/api/commission?action=referrals`. +- **Commission Details**: Links to existing `pages/commission` which handles full commission records. + +## Files Modified +- `pages/team/team.wxml` +- `pages/team/team.wxss` +- `pages/team/team.js` +- `pages/profile/profile.wxml` +- `pages/profile/profile.wxss` +- `pages/profile/profile.js` diff --git a/utils/api.js b/utils/api.js new file mode 100644 index 0000000..24e4aea --- /dev/null +++ b/utils/api.js @@ -0,0 +1,1856 @@ +/** + * API服务层 + * 封装所有后端API调用 + */ + +const config = require('../config/index') + +// ==================== 请求封装 ==================== + +/** + * 基础请求方法 + * @param {string} url - API路径 + * @param {object} options - 请求选项 + * @returns {Promise} + */ +const request = (url, options = {}) => { + return new Promise((resolve, reject) => { + const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) + + wx.request({ + url: config.API_BASE_URL + url, + method: options.method || 'GET', + data: options.data, + timeout: config.REQUEST_TIMEOUT, + header: { + 'Authorization': token ? `Bearer ${token}` : '', + 'Content-Type': 'application/json', + // 注意:x-user-id 已废弃,后端通过 Token 验证用户身份 + ...options.header + }, + success: (res) => { + // 始终打印简短的请求和响应信息用于调试 + console.log(`[API] ${options.method || 'GET'} ${url} -> ${res.statusCode}`) + + // 如果返回的是 HTML (通常是 404 页面),打印简短提示而非全文 + const isHtml = typeof res.data === 'string' && res.data.trim().startsWith(' { + console.error(`[API Error] ${url}`, err) + // 不自动显示toast,让调用方处理错误 + reject({ + code: -1, + message: '网络错误', + errMsg: err.errMsg || 'request:fail', + error: err + }) + } + }) + }) +} + +/** + * 文件上传 + * @param {string} filePath - 本地文件路径 + * @param {string} folder - 存储目录 (avatar/image/audio) + * @returns {Promise} + */ +const uploadFile = (filePath, folder = 'uploads') => { + return new Promise((resolve, reject) => { + console.log('[uploadFile] ========== 开始上传任务 ==========') + console.log('[uploadFile] 文件路径:', filePath) + console.log('[uploadFile] 目标目录:', folder) + + // 验证目录是否合法,不合法则使用默认的 uploads + const allowedFolders = ['uploads', 'image', 'images', 'avatars', 'characters', 'audio', 'documents', 'temp', 'assets'] + if (!allowedFolders.includes(folder)) { + console.warn(`[uploadFile] 目录 "${folder}" 不在允许列表中,将使用 "uploads"`) + folder = 'uploads' + } + + const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) + const userId = wx.getStorageSync(config.STORAGE_KEYS.USER_ID) + const deviceId = wx.getStorageSync('deviceId') || userId || 'unknown' + + // 始终包含 x-device-id,如果有 token 则包含 Authorization + const header = { + 'x-device-id': deviceId + } + + if (token) { + header['Authorization'] = `Bearer ${token}` + console.log('[uploadFile] 使用Token认证') + } else { + header['x-device-id'] = deviceId + console.log('[uploadFile] 使用DeviceId认证:', deviceId) + } + + wx.uploadFile({ + url: config.API_BASE_URL + '/upload', + filePath: filePath, + name: 'file', + formData: { folder }, + header: header, + success: (res) => { + console.log('[uploadFile] ========== 上传响应 ==========') + console.log('[uploadFile] 状态码:', res.statusCode) + console.log('[uploadFile] 响应头:', JSON.stringify(res.header)) + console.log('[uploadFile] 响应体:', res.data) + + if (res.statusCode === 200) { + try { + const data = JSON.parse(res.data) + console.log('[uploadFile] 解析后的数据:', JSON.stringify(data)) + + // 兼容两种响应格式 + if (data.code === 0 && data.data) { + console.log('[uploadFile] ✓ 上传成功 (code=0):', data.data.url) + resolve({ success: true, data: data.data }) + } else if (data.success && data.data) { + console.log('[uploadFile] ✓ 上传成功 (success=true):', data.data.url) + resolve(data) + } else { + const errMsg = data.message || '上传失败' + console.error('[uploadFile] ✗ 业务错误:', errMsg) + reject({ code: data.code || -1, message: errMsg }) + } + } catch (e) { + console.error('[uploadFile] ✗ 解析响应JSON失败:', e) + reject({ code: -1, message: '服务器响应格式错误' }) + } + } else if (res.statusCode === 500) { + let errorData = {} + try { + errorData = JSON.parse(res.data) + } catch (e) {} + + console.error('[uploadFile] ✗ 服务器内部错误 (500):', errorData) + reject({ + code: 500, + message: errorData.message || '服务器内部错误,请稍后重试', + detail: errorData + }) + } else if (res.statusCode === 413) { + console.error('[uploadFile] ✗ 文件太大 (413)') + reject({ code: 413, message: '文件太大,请选择较小的图片' }) + } else if (res.statusCode === 401 || res.statusCode === 403) { + console.error('[uploadFile] ✗ 权限错误 (401/403)') + reject({ code: res.statusCode, message: '权限不足,请重新登录' }) + } else { + console.error('[uploadFile] ✗ HTTP错误:', res.statusCode) + reject({ code: res.statusCode, message: `上传失败 (${res.statusCode})` }) + } + }, + fail: (err) => { + console.error('[uploadFile] ✗ wx.uploadFile 接口调用失败:', err) + reject({ code: -1, message: '网络连接失败,请检查网络' }) + } + }) + }) +} + +// ==================== 认证模块 API ==================== + +const auth = { + /** + * 发送短信验证码 + * @param {string} phone - 手机号 + */ + sendSms: (phone) => request('/auth/send-sms', { + method: 'POST', + data: { phone } + }), + + /** + * 手机号登录 + * @param {string} phone - 手机号 + * @param {string} code - 验证码 + */ + phoneLogin: (phone, code) => request('/auth/phone-login', { + method: 'POST', + data: { phone, code } + }), + + /** + * 微信手机号快速登录 + * @param {string} code - 微信getPhoneNumber返回的code + */ + wxPhoneLogin: (code, loginCode) => request('/auth/wx-phone-login', { + method: 'POST', + data: { code, loginCode } + }), + + /** + * 微信登录 + * @param {string} code - 微信登录code + * @param {object} userInfo - 用户信息(可选) + */ + wxLogin: (code, userInfo = null) => request('/auth/wx-login', { + method: 'POST', + data: { code, userInfo } + }), + + /** + * 获取当前用户信息 + */ + getCurrentUser: () => request('/auth/me'), + + /** + * 退出登录 + */ + logout: () => request('/auth/logout', { method: 'POST' }) +} + +// ==================== 用户模块 API ==================== + +const user = { + /** + * 获取用户资料 + */ + getProfile: () => request('/users/profile'), + + /** + * 更新用户资料 + * @param {object} data - 用户资料 + */ + updateProfile: (data) => request('/users/profile', { + method: 'PUT', + data + }), + + /** + * 获取用户余额 + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getBalance: () => request('/user/balance'), + + /** + * 获取余额历史 + * @param {object} params - 分页参数 + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getBalanceHistory: (params = {}) => request('/user/balance/history', { + data: params + }), + + /** + * 绑定手机号(微信授权方式) + * @param {object} data - { code: 微信授权code } + */ + bindPhone: (data) => request('/users/bindPhone', { + method: 'POST', + data: data + }), + + /** + * 获取用户记忆/档案 + * @param {string} characterId - 角色ID(可选) + */ + getMemories: (characterId = null) => { + const params = characterId ? { character_id: characterId } : {} + return request('/memory/memories', { data: params }) + }, + + /** + * 获取用户档案 + */ + getMemoryProfile: () => request('/memory/profile') +} + +// ==================== 角色模块 API ==================== + +const character = { + /** + * 获取角色列表 + * @param {object} params - 查询参数 { page, limit, gender, category } + */ + getList: (params = {}) => request('/characters', { + data: { page: 1, limit: 20, ...params } + }), + + /** + * 获取角色详情 + * @param {string} id - 角色ID + */ + getDetail: (id) => request(`/characters/${id}`), + + /** + * 随机推荐角色 + * @param {number} count - 数量 + * @param {object} options - 可选参数 + * @param {Array} options.excludeIds - 排除的角色ID列表 + * @param {string} options.gender - 性别筛选 (male/female) + */ + getRandom: (count = 6, options = {}) => { + const params = { count } + if (options.excludeIds && options.excludeIds.length > 0) { + params.excludeIds = options.excludeIds.join(',') + } + if (options.gender) { + params.gender = options.gender + } + return request('/characters/random', { data: params }) + }, + + /** + * 喜欢/取消喜欢角色 + * @param {string} id - 角色ID + */ + toggleLike: (id) => request(`/characters/${id}/like`, { + method: 'POST' + }), + + /** + * 获取喜欢的角色列表 + */ + getLikedList: () => request('/characters/liked'), + + /** + * 解锁角色 + * @param {object} data - { character_id, unlock_type: 'hearts'|'payment' } + */ + unlock: (data) => request('/characters/unlock', { + method: 'POST', + data + }), + + /** + * 检查角色是否已解锁 + * @param {string} id - 角色ID + */ + checkUnlockStatus: (id) => request(`/characters/${id}/unlock-status`, { + silent: true + }) +} + +// ==================== 聊天模块 API ==================== + +const chat = { + /** + * 发送AI聊天消息(使用简化版非流式API) + * @param {object} data - { character_id, message, conversation_id } + */ + sendMessage: (data) => request('/chat/simple', { + method: 'POST', + data + }), + + /** + * 获取聊天配额状态 + * @param {string} characterId - 角色ID + */ + getQuota: (characterId) => request('/chat/quota', { + data: { character_id: characterId }, + silent: true + }), + + /** + * 消费聊天配额 + * @param {string} characterId - 角色ID + */ + consumeQuota: (characterId) => request('/chat/quota/consume', { + method: 'POST', + data: { character_id: characterId } + }), + + /** + * 获取会话列表 + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getConversations: () => request('/conversations', { silent: true }), + + /** + * 删除会话 + * @param {string} conversationId - 会话ID + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + deleteConversation: (conversationId) => request(`/conversations/${conversationId}`, { + method: 'DELETE' + }), + + /** + * 清空聊天记录(保留会话) + * @param {string} characterId - 角色ID + * 注意:只清空聊天记录,会话仍然显示在消息列表中 + * 使用后端提供的 /api/memory/chat-history/by-character 接口 + */ + clearChatHistory: (characterId) => request(`/memory/chat-history/by-character?character_id=${characterId}&action=clear`, { + method: 'POST' + }), + + /** + * 创建会话 + * @param {string} characterId - 角色ID + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + createConversation: (characterId) => request('/conversations', { + method: 'POST', + data: { + targetId: characterId, + targetType: 'character' + } + }), + + /** + * 获取聊天历史(通过会话ID) + * @param {string} conversationId - 会话ID + * @param {object} params - 分页参数 + */ + getChatHistory: (conversationId, params = {}) => request('/memory/chat-history', { + data: { conversation_id: conversationId, ...params } + }), + + /** + * 获取聊天历史(通过角色ID) + * @param {string} characterId - 角色ID + * @param {object} params - 分页参数 + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getChatHistoryByCharacter: (characterId, params = {}) => { + // 手动构建查询参数字符串(微信小程序不支持 URLSearchParams) + const queryParts = [`character_id=${encodeURIComponent(characterId)}`] + + // 添加其他参数 + Object.keys(params).forEach(key => { + if (params[key] !== undefined && params[key] !== null) { + queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) + } + }) + + const queryString = queryParts.join('&') + const fullUrl = `/memory/chat-history/by-character?${queryString}` + + console.log('[API] getChatHistoryByCharacter 请求URL:', fullUrl) + console.log('[API] characterId:', characterId, 'params:', params) + + return request(fullUrl, { + method: 'GET', + silent: true // 静默模式,401时不清除登录状态 + }) + }, + + /** + * 标记会话已读 + * @param {string} conversationId - 会话ID + */ + markAsRead: (conversationId) => request(`/conversations/${conversationId}/read`, { + method: 'POST', + silent: true // 静默模式,401时不弹出登录提示 + }), + + /** + * 切换聊天模式(AI/真人) + * @param {string} conversationId - 会话ID + * @param {string} mode - 模式 (ai/human) + */ + switchMode: (conversationId, mode) => request(`/conversations/${conversationId}/switch-mode`, { + method: 'POST', + data: { mode } + }), + + /** + * 发送图片消息 + * @param {object} data - { character_id, conversation_id, image_url } + */ + sendImage: (data) => request('/chat/send-image', { + method: 'POST', + data + }), + + /** + * 获取免费畅聊时间 + */ + getFreeTime: () => request('/chat/free-time', { + silent: true + }), + + /** + * 领取免费畅聊时间 + */ + claimFreeTime: () => request('/chat/free-time/claim', { + method: 'POST' + }) +} + +// ==================== 陪聊模块 API ==================== + +const companion = { + /** + * 发送陪聊消息 + * @param {object} data - { order_id, message } + */ + sendMessage: (data) => request('/companion-chat', { + method: 'POST', + data + }), + + /** + * 获取陪聊师列表 + * 返回数据包含等级信息:levelCode, levelName, textPrice, voicePrice + * @param {object} params - 查询参数 { status, gender, sortBy, page, pageSize } + */ + getList: (params = {}) => request('/companions', { + data: { page: 1, pageSize: 20, ...params } + }), + + /** + * 获取陪聊师详情 + * 返回数据包含等级信息:levelCode, levelName, textPrice, voicePrice + * @param {string} id - 陪聊师ID + */ + getDetail: (id) => request(`/companions/${id}`), + + /** + * 获取陪聊师状态(包含等级信息) + * 返回数据包含:isCompanion, companionId, levelCode, levelName, textPrice, voicePrice + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getStatus: () => request('/companion/status'), + + /** + * 更新在线状态 + * @param {string} status - 状态 (online/busy/offline) + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + updateStatus: (status) => request('/companion/status', { + method: 'POST', + data: { status } + }), + + /** + * 获取申请状态 + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getApplyStatus: () => request('/companion/apply'), + + /** + * 申请成为陪聊师 + * @param {object} data - 申请资料 + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + apply: (data) => request('/companion/apply', { + method: 'POST', + data + }), + + /** + * AI辅助回复 + * @param {object} data - { message, context } + */ + aiAssist: (data) => request('/companion/ai-assist', { + method: 'POST', + data + }), + + /** + * 获取工作台数据(包含等级信息) + * 返回数据包含:levelCode, levelName, textPrice, voicePrice + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getWorkbench: () => request('/companion/workbench'), + + /** + * 获取客户列表 + * @param {object} params - 查询参数 { keyword, page, pageSize } + */ + getCustomers: (params = {}) => request('/companion/customers', { + data: params + }), + + /** + * 获取陪聊师订单列表 + * @param {object} params - 查询参数 { status, page, pageSize } + */ + getOrders: (params = {}) => request('/companion/orders', { + data: params + }), + + /** + * 获取陪聊师订单统计 + */ + getOrderStats: () => request('/companion/orders/stats'), + + /** + * 获取所有等级配置 + * 返回等级列表:[{ levelCode, levelName, textPrice, voicePrice }] + */ + getLevels: () => request('/companion/levels'), + + /** + * 获取陪聊师评价列表 + * @param {string} companionId - 陪聊师ID + * @param {object} params - 查询参数 { page, limit } + */ + getReviews: (companionId, params = {}) => request(`/companions/${companionId}/reviews`, { + data: { page: 1, limit: 10, ...params } + }), + + /** + * 点赞评价 + * @param {string} reviewId - 评价ID + */ + likeReview: (reviewId) => request(`/reviews/${reviewId}/like`, { + method: 'POST' + }), + + /** + * 陪聊师回复评价 + * @param {string} reviewId - 评价ID + * @param {string} reply - 回复内容 + */ + replyReview: (reviewId, reply) => request(`/reviews/${reviewId}/reply`, { + method: 'POST', + data: { reply } + }) +} + +// ==================== 语音合成 API ==================== + +const tts = { + /** + * 文字转语音 + * @param {object} data - { text, voice_id, character_id } + */ + synthesize: (data) => request('/tts', { + method: 'POST', + data + }), + + /** + * 查询TTS状态 + * @param {string} taskId - 任务ID + */ + getStatus: (taskId) => request('/tts/status', { + data: { task_id: taskId } + }) +} + +// ==================== 语音识别 API ==================== + +const speech = { + /** + * 语音识别 - 将语音转换为文字 + * @param {object} data - { audio: base64编码的音频, format: 音频格式(mp3/wav/pcm) } + * @returns {Promise<{success: boolean, data: {text: string}}>} + */ + recognize: (data) => request('/speech/recognize', { + method: 'POST', + data + }) +} + +// ==================== 支付模块 API ==================== + +const payment = { + /** + * 获取充值套餐 + */ + getPackages: () => request('/payment/packages'), + + /** + * 创建充值订单 + * @param {object} data - { packageId, amount, paymentMethod } + */ + createRechargeOrder: (data) => request('/payment/recharge', { + method: 'POST', + data + }), + + /** + * 创建VIP订单 + * @param {object} data - { planId, duration, paymentMethod } + */ + createVipOrder: (data) => request('/payment/vip', { + method: 'POST', + data + }), + + /** + * 获取支付参数(统一下单) + * @param {object} data - { orderId, orderType } + */ + prepay: (data) => request('/payment/prepay', { + method: 'POST', + data + }), + + /** + * 查询订单状态 + * @param {string} orderId - 订单ID + * @param {object} params - 查询参数 { confirm: 1 } + */ + queryOrder: (orderId, params = {}) => request(`/payment/orders/${orderId}`, { + method: 'GET', + data: params + }), + + /** + * 取消订单 + * @param {string} orderId - 订单ID + */ + cancelOrder: (orderId) => request(`/payment/orders/${orderId}/cancel`, { + method: 'POST' + }), + + /** + * 测试模式充值 - 直接增加爱心余额(仅测试环境使用) + * @param {object} data - 充值参数 + * @param {number} data.amount - 充值爱心数量 + * @param {string} data.package_name - 套餐名称(可选) + */ + testRecharge: (data) => request('/payment/test-recharge', { + method: 'POST', + data + }), + + /** + * 测试模式支付 - 直接标记订单为已支付(仅测试环境使用) + * @param {object} data - 支付参数 + * @param {string} data.orderId - 订单ID + */ + testPay: (data) => request('/payment/test-pay', { + method: 'POST', + data + }), + + /** + * 创建统一支付订单 + * @param {object} data - 订单参数 + * @param {string} data.orderType - 订单类型: recharge|vip|companion_chat|agent_purchase|identity_card + * @param {number} data.amount - 支付金额 + * @param {number} data.rechargeValue - 充值金额(充值订单) + * @param {string} data.vipType - VIP类型: month|quarter|year(VIP订单) + * @param {string} data.agentId - 智能体ID(智能体订单) + * @param {string} data.companionId - 陪聊师ID(陪聊订单) + * @param {number} data.duration - 陪聊时长(陪聊订单) + * @param {string} data.cardType - 身份卡类型: basic|premium(身份卡订单) + * @param {string} data.referralCode - 推荐码(可选) + * @param {string} data.promotionLinkCode - 推广链接码(可选) + * @param {string} data.confirm - 是否主动确认支付状态 (1) + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + createUnifiedOrder: (data) => request('/payment/unified-order', { + method: 'POST', + data + }), + + /** + * 创建支付订单(兼容旧接口) + * @param {object} data - { package_id, payment_method } + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + createOrder: (data) => request('/payment/unified-order', { + method: 'POST', + data + }), + + /** + * 获取订单列表 + * @param {object} params - 查询参数 + * @param {string} params.orderType - 订单类型(可选) + * @param {string} params.status - 订单状态(可选) + * @param {number} params.page - 页码 + * @param {number} params.pageSize - 每页数量 + * @param {string} params.confirm - 是否主动确认支付状态 (1) + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getOrders: (params = {}) => request('/payment/unified-order', { + data: { page: 1, pageSize: 20, ...params } + }), + + /** + * 获取订单详情 + * @param {string} orderId - 订单ID + * @param {object} params - { confirm: 1 } + */ + getOrderDetail: (orderId, params = {}) => request('/payment/unified-order', { + data: { orderId, ...params } + }), + + /** + * 查询支付状态 + * @param {string} orderId - 订单ID + * @param {object} params - { confirm: 1 } + */ + checkStatus: (orderId, params = {}) => request('/payment/unified-order', { + data: { orderId, ...params } + }), + + /** + * 使用爱心值兑换套餐 + * @param {object} data - 兑换参数 + * @param {string} data.packageId - 套餐ID (first/month/quarter/year) + * @param {number} data.heartCost - 需要的爱心值数量 + */ + exchangeWithHeart: (data) => request('/payment/exchange-with-heart', { + method: 'POST', + data + }) +} + +// ==================== 智能体商品 API ==================== + +const agentProduct = { + /** + * 获取商品列表 + * @param {object} params - 查询参数 + * @param {string} params.status - 状态(默认active) + * @param {number} params.page - 页码 + * @param {number} params.pageSize - 每页数量 + */ + getList: (params = {}) => request('/agent-products', { + data: { status: 'active', page: 1, pageSize: 20, ...params } + }), + + /** + * 获取商品详情 + * @param {string} id - 商品ID + */ + getDetail: (id) => request(`/agent-products/${id}`), + + /** + * 购买智能体 + * @param {string} id - 商品ID + * @param {object} options - 可选参数 + * @param {string} options.referralCode - 推荐码 + * @param {string} options.promotionLinkCode - 推广链接码 + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + purchase: (id, options = {}) => request(`/agent-products/${id}/purchase`, { + method: 'POST', + data: options + }), + + /** + * 获取用户已购智能体列表 + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getPurchased: () => request('/user/purchased-agents'), + + /** + * 检查是否有权访问某角色 + * @param {string} characterId - 角色ID + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + checkAccess: (characterId) => request('/user/purchased-agents', { + data: { action: 'checkAccess', characterId } + }), + + /** + * 获取可访问的角色ID列表 + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getAccessibleIds: () => request('/user/purchased-agents', { + data: { action: 'accessibleIds' } + }) +} + +// ==================== 页面素材 API ==================== + +const pageAssets = { + /** + * 获取通用素材配置 + * @param {string} group - 素材分组 + */ + getAssets: (group = null) => { + const url = group ? `/page-assets?group=${group}` : '/page-assets' + return request(url) + }, + + /** + * 获取服务页在线Banner列表 + */ + getServiceBanners: () => request('/page-assets/service-banners'), + + /** + * 获取娱乐页在线Banner列表 + */ + getEntertainmentBanners: () => request('/page-assets/entertainment-banners'), + + /** + * 获取合作入驻页在线Banner列表 + */ + getCooperationBanners: () => request('/page-assets/cooperation-banners') +} + +// ==================== 推广系统 API ==================== + +const promotion = { + /** + * 创建推广链接 + * @param {object} data - 链接参数 + * @param {string} data.targetType - 目标类型: agent|vip|recharge|general + * @param {string} data.targetId - 目标ID(智能体推广时需要) + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + createLink: (data) => request('/promotion', { + method: 'POST', + data + }), + + /** + * 获取推广链接列表 + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getLinks: () => request('/promotion'), + + /** + * 获取推广统计 + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getStats: () => request('/promotion', { + data: { action: 'stats' } + }), + + /** + * 获取链接详情 + * @param {string} linkId - 链接ID + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getLinkDetail: (linkId) => request('/promotion', { + data: { action: 'detail', linkId } + }), + + /** + * 获取分销商排行榜 + * @param {object} params - 查询参数 + * @param {string} params.period - 周期: day|week|month|all + * @param {number} params.limit - 返回数量 + */ + getRanking: (params = {}) => request('/promotion', { + data: { action: 'ranking', period: 'month', limit: 10, ...params } + }), + + /** + * 获取分享配置 + * @param {string} pageKey - 页面标识: index|promote 等 + * 返回: { title, desc, path, imageUrl } + */ + getShareConfig: (pageKey) => request('/promotion/share-config', { + data: { page: pageKey } + }), + + /** + * 记录分享行为 + * @param {object} data - 分享参数 + * @param {string} data.type - 分享类型: app_message|timeline + * @param {string} data.page - 页面路径 + * @param {string} data.referralCode - 推荐码(可选) + */ + recordShare: (data) => request('/promotion', { + method: 'POST', + data: { action: 'recordShare', ...data } + }) +} + +// ==================== 订单模块 API ==================== + +const order = { + /** + * 获取订单列表 + * @param {object} params - { type, status, page, limit } + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getList: (params = {}) => request('/orders', { + data: { page: 1, pageSize: 20, ...params } + }), + + /** + * 获取订单详情 + * @param {string} id - 订单ID + */ + getDetail: (id) => request(`/orders/${id}`), + + /** + * 创建陪聊订单 + * @param {object} data - { companion_id, duration, message } + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + createCompanionOrder: (data) => request('/orders/companion', { + method: 'POST', + data + }), + + /** + * 取消订单 + * @param {string} id - 订单ID + * @param {string} reason - 取消原因 + */ + cancel: (id, reason = '') => request(`/orders/${id}/cancel`, { + method: 'POST', + data: { reason } + }), + + /** + * 评价订单 + * @param {string} id - 订单ID + * @param {object} data - { rating, content, tags, isAnonymous } + */ + review: (id, data) => request(`/orders/${id}/review`, { + method: 'POST', + data + }), + + /** + * 获取订单大厅(陪聊师用) + */ + getHall: () => request('/orders/hall'), + + /** + * 接受订单 + * @param {string} id - 订单ID + */ + accept: (id) => request(`/orders/${id}/accept`, { method: 'POST' }), + + /** + * 拒绝订单 + * @param {string} id - 订单ID + * @param {string} reason - 拒绝原因 + */ + reject: (id, reason) => request(`/orders/${id}/reject`, { + method: 'POST', + data: { reason } + }), + + /** + * 开始服务 + * @param {string} id - 订单ID + */ + startService: (id) => request(`/orders/${id}/start-service`, { method: 'POST' }), + + /** + * 结束服务 + * @param {string} id - 订单ID + */ + endService: (id) => request(`/orders/${id}/end-service`, { method: 'POST' }) +} + + +// ==================== 佣金与提现 API ==================== + +const commission = { + /** + * 获取佣金统计 + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getStats: () => request('/commission', { data: { action: 'stats' } }), + + /** + * 获取佣金记录 + * @param {object} params - 分页参数 { page, pageSize } + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getRecords: (params = {}) => request('/commission', { + data: { action: 'records', ...params } + }), + + /** + * 获取推荐列表 + * @param {object} params - 分页参数 { page, pageSize } + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getReferrals: (params = {}) => request('/commission', { + data: { action: 'referrals', ...params } + }), + + /** + * 获取提现记录 + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getWithdrawals: () => request('/commission', { + data: { action: 'withdrawals' } + }), + + /** + * 绑定推荐码 + * @param {string} referralCode - 推荐码 + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + bindReferral: (referralCode) => request('/commission', { + method: 'POST', + data: { action: 'bindReferral', referralCode } + }), + + /** + * 申请提现 + * @param {object} data - 提现参数 + * @param {number} data.amount - 提现金额 + * @param {string} data.withdrawType - 提现方式: wechat|alipay|bank + * @param {object} data.accountInfo - 账户信息 { name, account } + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + withdraw: (data) => request('/commission', { + method: 'POST', + data: { action: 'withdraw', ...data } + }) +} + +// ==================== 交易流水 API ==================== + +const transaction = { + /** + * 获取交易流水 + * @param {object} params - 查询参数 + * @param {string} params.type - 交易类型(可选) + * @param {string} params.status - 状态(可选) + * @param {number} params.page - 页码 + * @param {number} params.pageSize - 每页数量 + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getList: (params = {}) => request('/user/transactions', { + data: { page: 1, pageSize: 20, ...params } + }), + + /** + * 获取余额统计 + * 注意:后端通过 Token 验证用户身份,不再需要传递 userId + */ + getStats: () => request('/user/transactions', { + data: { action: 'stats' } + }) +} + +const withdraw = { + /** + * 申请提现 + * @param {object} data - { amount, account_type, account_info } + */ + apply: (data) => request('/withdraw/apply', { + method: 'POST', + data + }), + + /** + * 获取提现记录 + * @param {object} params - 分页参数 + */ + getRecords: (params = {}) => request('/withdraw/records', { + data: params + }) +} + +// ==================== 社交广场 API ==================== + +const post = { + /** + * 获取动态列表 + * @param {object} params - { page, limit, type } + */ + getList: (params = {}) => request('/posts', { + data: { page: 1, limit: 20, ...params } + }), + + /** + * 发布动态 + * @param {object} data - { content, images } + */ + create: (data) => request('/posts', { + method: 'POST', + data + }), + + /** + * 获取动态详情 + * @param {string} id - 动态ID + */ + getDetail: (id) => request(`/posts/${id}`), + + /** + * 删除动态 + * @param {string} id - 动态ID + */ + delete: (id) => request(`/posts/${id}`, { method: 'DELETE' }), + + /** + * 点赞/取消点赞 + * @param {string} id - 动态ID + */ + toggleLike: (id) => request(`/posts/${id}/like`, { method: 'POST' }), + + /** + * 发表评论 + * @param {string} postId - 动态ID + * @param {string} content - 评论内容 + */ + addComment: (postId, content) => request(`/posts/${postId}/comments`, { + method: 'POST', + data: { content } + }), + + /** + * 删除评论 + * @param {string} postId - 动态ID + * @param {string} commentId - 评论ID + */ + deleteComment: (postId, commentId) => request(`/posts/${postId}/comments/${commentId}`, { + method: 'DELETE' + }) +} + +// ==================== 背包道具 API ==================== + +const backpack = { + /** + * 获取背包物品 + */ + getItems: () => request('/backpack'), + + /** + * 使用物品 + * @param {string} itemId - 物品ID + */ + useItem: (itemId) => request('/backpack/use', { + method: 'POST', + data: { item_id: itemId } + }), + + /** + * 赠送礼物 + * @param {object} data - { item_id, target_id, target_type, quantity } + */ + giftItem: (data) => request('/backpack/gift', { + method: 'POST', + data + }), + + /** + * 装备道具 + * @param {string} itemId - 物品ID + */ + equipItem: (itemId) => request('/backpack/equip', { + method: 'POST', + data: { item_id: itemId } + }) +} + +// ==================== 商城 API ==================== + +const shop = { + /** + * 获取商城物品 + * @param {object} params - { category } + */ + getItems: (params = {}) => request('/shop/items', { + data: params + }), + + /** + * 购买物品 + * @param {object} data - { item_id, quantity } + */ + buyItem: (data) => request('/shop/buy', { + method: 'POST', + data + }) +} + +// ==================== 客服系统 API ==================== + +const customerService = { + /** + * 创建咨询 (开启新会话) + * @param {object} data - { category, content, userName, userPhone, subject, guestId } + */ + create: (data) => request('/customer-service', { + method: 'POST', + data: { action: 'create', ...data } + }), + + /** + * 发送回复消息 + * @param {object} data - { ticketId, content, userName } + */ + reply: (data) => request('/customer-service', { + method: 'POST', + data: { action: 'reply', ...data } + }), + + /** + * 获取咨询列表 + * @param {string} guestId - 访客唯一ID + */ + getList: (guestId) => request(`/customer-service?guestId=${guestId}`), + + /** + * 获取咨询详情及消息记录 + * @param {string} ticketId - 工单ID + */ + getDetail: (ticketId) => request(`/customer-service?ticketId=${ticketId}`) +} + +// ==================== 系统设置 API ==================== + +const settings = { + /** + * 获取用户设置 + */ + get: () => request('/settings'), + + /** + * 更新用户设置 + * @param {object} data - 设置数据 + */ + update: (data) => request('/settings', { + method: 'PUT', + data + }), + + /** + * 获取黑名单 + */ + getBlacklist: () => request('/settings/blacklist'), + + /** + * 添加黑名单 + * @param {string} userId - 用户ID + */ + addToBlacklist: (userId) => request('/settings/blacklist', { + method: 'POST', + data: { user_id: userId } + }), + + /** + * 移除黑名单 + * @param {string} userId - 用户ID + */ + removeFromBlacklist: (userId) => request(`/settings/blacklist/${userId}`, { + method: 'DELETE' + }), + + /** + * 清除缓存 + */ + clearCache: () => request('/settings/clear-cache', { method: 'POST' }), + + /** + * 检查更新 + */ + checkUpdate: () => request('/settings/check-update'), + + /** + * 提交反馈 + * @param {object} data - { type, content, contact } + */ + submitFeedback: (data) => request('/settings/feedback', { + method: 'POST', + data + }) +} + +// ==================== 公共接口 API ==================== + +const common = { + /** + * 获取审核状态 + * 1 表示开启(审核中),0 表示关闭(正式环境) + */ + getAuditStatus: () => request('/common/audit-status'), + + /** + * 获取品牌配置 + * 返回品牌故事、介绍等内容 + */ + getBrandConfig: () => request('/public/brand-config'), + + /** + * 获取公告列表 + * 返回启用的公告列表(按 sortOrder 升序排列) + */ + getNotices: () => request('/public/entertainment/notices'), + + /** + * 获取公告详情 + * @param {number} id - 公告ID + */ + getNoticeDetail: (id) => request(`/public/entertainment/notices/${id}`) +} + +// ==================== 协议模块 API ==================== + +const agreement = { + /** + * 获取协议内容 + * @param {string} code - 协议代码 (phone-guide, user-agreement, privacy-policy) + */ + get: (code) => request(`/agreements/${code}`) +} + +// ==================== 图片回复话术 API ==================== + +const imageReply = { + /** + * 获取随机图片回复话术 + * 用于用户发送图片时,AI返回预设的话术回复 + */ + getRandom: () => request('/image-reply/random') +} + +// ==================== 主动推送消息 API ==================== + +const proactiveMessage = { + /** + * 获取待推送消息 + * 返回AI角色主动发送的消息列表(跟进消息和打招呼消息) + * 只返回未读(read_at IS NULL)的消息 + */ + getPending: () => request('/proactive-messages/pending', { silent: true }), + + /** + * 标记消息已读 + * @param {object} data - 参数(二选一) + * @param {string} data.character_id - 按角色ID标记(推荐) + * @param {Array} data.message_ids - 按消息ID列表标记 + */ + markAsRead: (data) => request('/proactive-messages/read', { + method: 'POST', + data, + silent: true + }) +} + +// ==================== 活动模块 API ==================== + +const activity = { + /** + * 获取活动列表 + * @param {object} params - 查询参数 + * @param {string} params.tab - 活动标签:featured(精选)、newbie(新手) + * @param {string} params.category - 活动分类:city(同城)、outdoor(户外)、travel(旅行)、featured(精选) + * @param {string} params.city - 城市筛选 + * @param {string} params.keyword - 关键词搜索 + * @param {string} params.priceType - 价格类型:free(免费)、paid(付费) + * @param {string} params.dateFrom - 开始日期筛选 (YYYY-MM-DD) + * @param {string} params.dateTo - 结束日期筛选 (YYYY-MM-DD) + * @param {string} params.sortBy - 排序方式:date(日期)、likes(点赞数)、participants(参与人数) + * @param {number} params.page - 页码 + * @param {number} params.limit - 每页数量 + */ + getList: (params = {}) => request('/entertainment/activities', { data: params }), + + /** + * 获取活动详情 + * @param {string} id - 活动ID + */ + getDetail: (id) => request(`/entertainment/activities/${id}`), + + /** + * 获取活动参与者列表 + * @param {string} id - 活动ID + * @param {object} params - 分页参数 { page, limit } + */ + getParticipants: (id, params = {}) => request(`/entertainment/activities/${id}/registrations`, { + data: params + }), + /** + * 报名活动 + * @param {string} id - 活动ID + */ + signup: (id) => request(`/entertainment/activities/${id}/signup`, { + method: 'POST' + }), + + /** + * 取消报名 + * @param {string} id - 活动ID + */ + cancelSignup: (id) => request(`/entertainment/activities/${id}/signup`, { + method: 'DELETE' + }), + + /** + * 点赞/收藏活动 + * @param {string} id - 活动ID + */ + favorite: (id) => request(`/entertainment/activities/${id}/like`, { + method: 'POST' + }), + + /** + * 取消点赞/收藏活动 + * @param {string} id - 活动ID + */ + unfavorite: (id) => request(`/entertainment/activities/${id}/like`, { + method: 'POST' + }), + + /** + * 取消点赞/取消点赞活动 + * @param {string} id - 活动ID + */ + toggleLike: (id) => request(`/entertainment/activities/${id}/like`, { + method: 'POST' + }), + + /** + * 获取我的报名列表 + * @param {object} params - { status, page, limit } + */ + getMyRegistrations: (params = {}) => request('/my-registrations', { data: params }) +} + +// ==================== 兴趣搭子 API ==================== + +const interest = { + /** + * 获取兴趣分类列表 + */ + getCategories: () => request('/interest-categories'), + + /** + * 获取兴趣搭子列表 + * @param {object} params - 查询参数 + * @param {number} params.page - 页码 + * @param {number} params.limit - 每页数量 + * @param {string} params.city - 城市筛选 + * @param {string} params.province - 省份筛选 + * @param {number} params.category_id - 分类ID筛选 + */ + getList: (params = {}) => request('/interest-partners', { data: params }), + + /** + * 获取兴趣搭子详情 + * @param {number} id - 搭子ID + */ + getDetail: (id) => request(`/interest-partners/${id}`) +} + +// ==================== 文娱页面 API ==================== + +const entertainment = { + /** + * 获取首页数据(包含轮播图、分类、公告、精选活动、新手活动) + * @param {string} city - 城市名称(可选) + */ + getHome: (city = null) => { + const params = city ? { city } : {} + return request('/entertainment/home', { data: params }) + }, + + /** + * 获取轮播Banner + */ + getBanners: () => request('/entertainment/banners'), + + /** + * 获取滚动公告 + */ + getNotices: () => request('/entertainment/notices'), + + /** + * 每日签到 + */ + checkin: () => request('/entertainment/checkin', { + method: 'POST' + }), + + /** + * 获取签到记录 + * @param {string} month - 月份 (YYYY-MM) + */ + getCheckinRecords: (month) => request('/entertainment/checkin/records', { + data: { month } + }), + + /** + * 爱心传递 + * @param {object} data - 传递信息 + */ + transferLove: (data) => request('/entertainment/love/transfer', { + method: 'POST', + data + }), + + /** + * 获取爱心榜 + * @param {string} type - 榜单类型 (day/week/month) + * @param {number} limit - 返回数量 + */ + getLoveRanking: (type = 'day', limit = 10) => + request('/entertainment/love/ranking', { + data: { type, limit } + }) +} + +// ==================== 爱心值系统 API ==================== + +const lovePoints = { + /** + * 记录分享获得爱心值 + */ + share: () => request('/love-points/share', { + method: 'POST' + }), + + /** + * 登录获得爱心值(B自己+100,分享人也可获得+100) + * 后端会检查是否有inviter参数,给分享人也+100 + */ + login: () => request('/love-points/login', { + method: 'POST' + }), + + /** + * 获取邀请码 + */ + getInvitationCode: () => request('/love-points/invitation-code'), + + /** + * 完善资料获得爱心值 + */ + profileComplete: () => request('/love-points/profile-complete', { + method: 'POST' + }), + + /** + * 获取爱心值流水记录 + * @param {object} params - 查询参数 { page, limit } + */ + getTransactions: (params = {}) => request('/love-points/transactions', { + data: { limit: 100, ...params } + }), + + /** + * 检查注册奖励领取资格 + */ + checkRegistrationReward: () => request('/love-points/registration-reward/check'), + + /** + * 领取注册奖励 + */ + claimRegistrationReward: () => request('/love-points/registration-reward', { + method: 'POST' + }), + + /** + * 检查 GF100 弹窗领取资格 + */ + checkGf100Status: () => request('/love-points/gf100'), + + /** + * 领取 GF100 奖励 + */ + claimGf100: () => request('/love-points/gf100', { + method: 'POST' + }) +} + +const loveExchange = { + /** + * 获取可兑换选项(包含当前爱心值余额) + */ + getOptions: () => request('/love-exchange/options'), + + /** + * 使用爱心值兑换会员 + * @param {object} data - { package_id, love_cost } + */ + exchangeVip: (data) => request('/love-exchange/vip', { + method: 'POST', + data + }) +} + +const gifts = { + /** + * 获取礼品列表 + * @param {object} params - 查询参数 { category, page, limit } + */ + getList: (params = {}) => request('/gifts', { + data: params + }), + + /** + * 获取礼品详情 + * @param {string} id - 礼品ID + */ + getDetail: (id) => request(`/gifts/${id}`), + + /** + * 兑换礼品 + * @param {object} data - { giftId, shippingInfo: { name, phone, address } } + */ + exchange: (data) => request('/gifts/exchange', { + method: 'POST', + data + }), + + /** + * 获取我的兑换记录 + * @param {object} params - 查询参数 { page, limit } + */ + getMyExchanges: (params = {}) => request('/gifts/my-exchanges', { + data: { limit: 50, ...params } + }) +} + +// ==================== 推荐关系 API ==================== + +const referral = { + /** + * 绑定推荐关系 + * @param {object} data - 绑定参数 + * @param {string} data.userId - 用户ID + * @param {string} data.referralCode - 推荐码 + */ + bind: (data) => request('/referral/bind', { + method: 'POST', + data + }), + + /** + * 保存待绑定推荐码 + * @param {object} data - 参数 + * @param {string} data.userId - 用户ID + * @param {string} data.referralCode - 推荐码 + */ + savePending: (data) => request('/referral/pending', { + method: 'POST', + data + }), + + /** + * 获取待绑定推荐码 + * @param {string} userId - 用户ID + */ + getPending: (userId) => request('/referral/pending', { + data: { userId } + }) +} + +// ==================== 快乐学堂 API ==================== + +const happySchool = { + /** + * 获取分类列表 + */ + getCategories: () => request('/xinban/categories'), + + /** + * 获取文章列表 + * @param {object} params - 查询参数 + * @param {number} params.categoryId - 分类ID + * @param {string} params.keyword - 关键词搜索 + * @param {number} params.page - 页码 + * @param {number} params.limit - 每页数量 + */ + getArticles: (params = {}) => request('/xinban/articles', { + data: { page: 1, limit: 20, ...params } + }), + + /** + * 获取文章详情 + * @param {number} id - 文章ID + */ + getArticleDetail: (id) => request(`/xinban/articles/${id}`), + + /** + * 点赞/取消点赞文章 + * @param {number} id - 文章ID + */ + toggleLike: (id) => request(`/xinban/articles/${id}/like`, { + method: 'POST' + }), + + /** + * 记录阅读 + * @param {number} id - 文章ID + * @param {number} readDuration - 阅读时长(秒) + */ + recordRead: (id, readDuration = 0) => request(`/xinban/articles/${id}/read`, { + method: 'POST', + data: { readDuration } + }) +} + +// ==================== 导出所有API ==================== + +module.exports = { + // 基础方法 + request, + uploadFile, + + // 模块API + auth, + user, + character, + chat, + companion, + tts, + speech, + payment, + agentProduct, + pageAssets, + promotion, + order, + commission, + transaction, + withdraw, + post, + backpack, + shop, + customerService, + settings, + common, + agreement, + imageReply, + proactiveMessage, + + // 娱乐页面相关API + activity, + interest, + entertainment, + + // 爱心值系统API + lovePoints, + loveExchange, + gifts, + + // 推荐关系API + referral, + + // 快乐学堂API + happySchool +} diff --git a/utils/assets.js b/utils/assets.js new file mode 100644 index 0000000..1447ff4 --- /dev/null +++ b/utils/assets.js @@ -0,0 +1,193 @@ +/** + * 页面素材管理工具类 + * 用于小程序获取和缓存后台配置的图片资源 + */ + +const config = require('../config/index'); +const API_BASE_URL = config.API_BASE_URL.replace('/api', ''); // 移除 /api 后缀 +const CACHE_KEY = 'pageAssets'; +const CACHE_DURATION = 30 * 60 * 1000; // 30分钟缓存 + +/** + * 获取页面素材配置 + * @param {string|null} group - 素材分组名称,不传则获取全部 + * @param {boolean} forceRefresh - 是否强制刷新,忽略缓存 + * @returns {Promise} 素材配置对象 + */ +async function getPageAssets(group = null, forceRefresh = false) { + try { + // 1. 如果不强制刷新,先尝试从缓存读取 + if (!forceRefresh) { + const cached = getCachedAssets(); + if (cached) { + return group ? cached[group] : cached; + } + } + + // 2. 从服务器获取 + const url = group + ? `${API_BASE_URL}/api/page-assets?group=${group}` + : `${API_BASE_URL}/api/page-assets`; + + const res = await wx.request({ + url, + method: 'GET', + timeout: 10000 + }); + + if (res.statusCode === 200 && res.data.success) { + // 3. 更新缓存(只有获取全部时才缓存) + if (!group) { + setCachedAssets(res.data.data); + } + + return res.data.data; + } + + // 4. 请求失败,返回默认配置 + console.warn('获取素材配置失败,使用默认配置'); + return getDefaultAssets(group); + + } catch (error) { + console.error('获取素材配置异常:', error); + return getDefaultAssets(group); + } +} + +/** + * 从缓存读取素材配置 + */ +function getCachedAssets() { + try { + const cached = wx.getStorageSync(CACHE_KEY); + if (cached && cached.timestamp && cached.data) { + if (Date.now() - cached.timestamp < CACHE_DURATION) { + return cached.data; + } + } + return null; + } catch (error) { + console.error('读取缓存失败:', error); + return null; + } +} + +/** + * 保存素材配置到缓存 + */ +function setCachedAssets(data) { + try { + wx.setStorageSync(CACHE_KEY, { + data, + timestamp: Date.now() + }); + } catch (error) { + console.error('保存缓存失败:', error); + } +} + +/** + * 清除素材缓存 + */ +function clearAssetsCache() { + try { + wx.removeStorageSync(CACHE_KEY); + console.log('素材缓存已清除'); + } catch (error) { + console.error('清除缓存失败:', error); + } +} + +/** + * 预加载素材配置(建议在 app.js 的 onLaunch 中调用) + */ +async function preloadAssets() { + try { + await getPageAssets(null, false); + console.log('素材配置预加载完成'); + } catch (error) { + console.error('素材配置预加载失败:', error); + } +} + +/** + * 获取默认素材配置(降级方案 - 使用CDN URL) + */ +function getDefaultAssets(group = null) { + const cdnBase = 'https://ai-c.maimanji.com/images' + const defaults = { + banners: { + companion_banner: `${cdnBase}/Header-banner.png`, + service_banner: `${cdnBase}/service-banner-1.png`, + home_banner: `${cdnBase}/Header-banner.png`, + medical_banner: `${cdnBase}/service-banner-2.png`, + housekeeping_banner: `${cdnBase}/service-banner-3.png`, + eldercare_banner: `${cdnBase}/service-banner-4.png`, + service_banner_5: `${cdnBase}/service-banner-5.png`, + service_banner_6: `${cdnBase}/service-banner-6.png`, + cooperation_banner: `${cdnBase}/Header-banner.png`, + }, + entries: { + entry_1: `${cdnBase}/pb01.png`, + entry_2: `${cdnBase}/pb02.png`, + entry_3: `${cdnBase}/pb03.png`, + entry_4: `${cdnBase}/pb04.png`, + }, + icons: { + consult_button: `${cdnBase}/btn-text-consult.png`, + gift: `${cdnBase}/icon-gift.png`, + location: `${cdnBase}/icon-location.png`, + }, + service_icons: { + medical: `${cdnBase}/icon-medical.png`, + flow: `${cdnBase}/icon-flow.png`, + advantage: `${cdnBase}/icon-advantage.png`, + professional: `${cdnBase}/icon-professional.png`, + safe: `${cdnBase}/icon-safe.png`, + efficient: `${cdnBase}/icon-efficient.png`, + care: `${cdnBase}/icon-care.png`, + }, + status_icons: { + pending: `${cdnBase}/icon-pending.png`, + success: `${cdnBase}/icon-success.png`, + rejected: `${cdnBase}/icon-rejected.png`, + }, + empty_states: { + orders: `${cdnBase}/empty-orders.png`, + messages: `${cdnBase}/empty-messages.png`, + }, + defaults: { + avatar: `${cdnBase}/default-avatar.png`, + placeholder: '/images/placeholder.jpg', + }, + tabbar: { + listen: `${cdnBase}/tab-listen.png`, + listen_active: `${cdnBase}/tab-listen-active.png`, + companion: `${cdnBase}/tab-companion.png`, + companion_active: `${cdnBase}/tab-companion-active.png`, + service: `${cdnBase}/tab-service.png`, + service_active: `${cdnBase}/tab-service-active.png`, + message: `${cdnBase}/tab-message.png`, + message_unread: `${cdnBase}/tab-message-nodot.png`, + message_active: `${cdnBase}/tab-message-active.png`, + profile: `${cdnBase}/tab-profile.png`, + profile_active: `${cdnBase}/tab-profile-active.png`, + square: `${cdnBase}/tab-compass.png`, + square_active: `${cdnBase}/tab-compass-active.png`, + }, + workbench: { + orders: `${cdnBase}/icon-orders.png`, + customers: `${cdnBase}/icon-customers.png`, + withdraw: `${cdnBase}/icon-withdraw.png`, + commission: `${cdnBase}/icon-commission.png`, + }, + }; + + return group ? defaults[group] : defaults; +} + +module.exports = { + getPageAssets, + clearAssetsCache, + preloadAssets +}; diff --git a/utils/auth.js b/utils/auth.js new file mode 100644 index 0000000..ece4f29 --- /dev/null +++ b/utils/auth.js @@ -0,0 +1,464 @@ +/** + * 登录认证工具类 + * 实现30天免登录功能(实际由后端Token有效期控制,当前为7天) + * + * 功能: + * - 本地登录状态检查 + * - 服务端Token验证 + * - 微信手机号快速登录 + * - 登录状态持久化 + */ + +const config = require('../config/index') +const api = require('./api') + +// 存储键名 +const TOKEN_KEY = config.STORAGE_KEYS.TOKEN +const USER_KEY = config.STORAGE_KEYS.USER_INFO +const USER_ID_KEY = config.STORAGE_KEYS.USER_ID +const TOKEN_EXPIRY_KEY = config.STORAGE_KEYS.TOKEN_EXPIRY + +/** + * 检查是否已登录(本地验证) + * 仅检查本地存储,不发起网络请求 + * @returns {boolean} 是否已登录 + */ +function isLoggedIn() { + const token = wx.getStorageSync(TOKEN_KEY) + const expiry = wx.getStorageSync(TOKEN_EXPIRY_KEY) + + if (!token) return false + + // 如果有过期时间,检查是否过期 + if (expiry) { + return new Date(expiry) > new Date() + } + + // 没有过期时间但有token,认为已登录(兼容旧数据) + return true +} + +/** + * 获取本地保存的用户信息 + * @returns {object|null} 用户信息 + */ +function getLocalUserInfo() { + return wx.getStorageSync(USER_KEY) || null +} + +/** + * 获取登录Token + * @returns {string|null} Token + */ +function getToken() { + return wx.getStorageSync(TOKEN_KEY) || null +} + +/** + * 获取用户ID + * @returns {string|null} 用户ID + */ +function getUserId() { + return wx.getStorageSync(USER_ID_KEY) || null +} + +/** + * 验证登录状态(服务端验证) + * 调用后端API验证Token是否有效 + * @returns {Promise<{valid: boolean, user?: object, expired?: boolean, error?: any}>} + */ +async function verifyLogin() { + const token = getToken() + if (!token) { + return { valid: false } + } + + try { + const res = await api.auth.getCurrentUser() + + if (res.success && res.data) { + // 更新本地用户信息 + saveUserInfo(res.data, token) + return { valid: true, user: res.data } + } else { + // Token无效,清除本地数据 + clearLoginInfo() + return { valid: false, expired: true } + } + } catch (error) { + console.error('验证登录失败:', error) + + // 401错误表示Token过期 + if (error.code === 401) { + clearLoginInfo() + return { valid: false, expired: true } + } + + // 网络错误等情况,保持本地状态 + return { valid: false, error } + } +} + +/** + * 微信手机号快速登录 + * @param {string} code - 微信getPhoneNumber返回的code + * @returns {Promise<{success: boolean, user?: object, error?: string}>} + */ +async function wxPhoneLogin(code, loginCode) { + try { + const res = await api.auth.wxPhoneLogin(code, loginCode) + + if (res.success && res.data) { + const { token, user } = res.data + + // 计算过期时间(7天后,与后端保持一致) + const expiresAt = new Date() + expiresAt.setDate(expiresAt.getDate() + 7) + + // 保存登录信息 + saveUserInfo(user, token, expiresAt.toISOString()) + + return { success: true, user } + } + + return { success: false, error: res.error || '登录失败' } + } catch (error) { + console.error('微信手机号登录失败:', error) + return { success: false, error: error.message || '网络错误' } + } +} + +/** + * 保存用户信息到本地 + * @param {object} user - 用户信息 + * @param {string} token - 登录Token + * @param {string} expiresAt - 过期时间(可选) + */ +function saveUserInfo(user, token, expiresAt = null) { + if (token) { + wx.setStorageSync(TOKEN_KEY, token) + } + + if (user) { + wx.setStorageSync(USER_KEY, user) + if (user.id) { + wx.setStorageSync(USER_ID_KEY, user.id) + } + } + + if (expiresAt) { + wx.setStorageSync(TOKEN_EXPIRY_KEY, expiresAt) + } + + // 登录成功后,检查并绑定推荐码 + if (user && user.id && token) { + checkAndBindReferralCode(user.id, token) + } +} + +/** + * 检查并绑定推荐码(佣金系统) + * 在用户登录成功后自动调用 + * 包含重试机制,确保网络波动时也能成功绑定 + * @param {string} userId - 用户ID + * @param {string} token - 登录token + * @param {number} retryCount - 重试次数(内部使用) + */ +async function checkAndBindReferralCode(userId, token, retryCount = 0) { + const MAX_RETRY = 3 + const REFERRAL_CODE_KEY = 'pendingReferralCode' + + try { + const referralCode = wx.getStorageSync(REFERRAL_CODE_KEY) + + if (!referralCode) { + console.log('[推荐码绑定] 没有待绑定的推荐码') + return + } + + console.log(`[推荐码绑定] 检测到待绑定推荐码: ${referralCode}, 重试次数: ${retryCount}`) + + const res = await api.commission.bindReferral(referralCode) + + if (res.success) { + wx.removeStorageSync(REFERRAL_CODE_KEY) + const app = getApp() + if (app && app.globalData) { + app.globalData.pendingReferralCode = null + } + console.log('[推荐码绑定] 推荐关系绑定成功') + } else { + console.log('[推荐码绑定] 推荐关系绑定失败:', res.error) + + if (res.error && ( + res.error.includes('已绑定') || + res.error.includes('自己') || + res.error.includes('无效') + )) { + wx.removeStorageSync(REFERRAL_CODE_KEY) + const app = getApp() + if (app && app.globalData) { + app.globalData.pendingReferralCode = null + } + console.log('[推荐码绑定] 已清除无效的推荐码') + } else if (retryCount < MAX_RETRY) { + console.log(`[推荐码绑定] 将在5秒后进行第${retryCount + 1}次重试...`) + await new Promise(resolve => setTimeout(resolve, 5000)) + await checkAndBindReferralCode(userId, token, retryCount + 1) + } else { + console.log('[推荐码绑定] 重试次数已用尽,绑定失败') + } + } + } catch (error) { + console.error('[推荐码绑定] 绑定推荐码失败:', error) + + if (retryCount < MAX_RETRY) { + console.log(`[推荐码绑定] 网络错误,5秒后进行第${retryCount + 1}次重试...`) + await new Promise(resolve => setTimeout(resolve, 5000)) + await checkAndBindReferralCode(userId, token, retryCount + 1) + } else { + console.log('[推荐码绑定] 重试次数已用尽,网络错误') + } + } +} + +/** + * 清除登录信息 + */ +function clearLoginInfo() { + wx.removeStorageSync(TOKEN_KEY) + wx.removeStorageSync(USER_KEY) + wx.removeStorageSync(USER_ID_KEY) + wx.removeStorageSync(TOKEN_EXPIRY_KEY) + wx.removeStorageSync(config.STORAGE_KEYS.REFRESH_TOKEN) +} + +/** + * 退出登录 + * @returns {Promise} + */ +async function logout() { + const token = getToken() + + if (token) { + try { + await api.auth.logout() + } catch (error) { + console.error('退出登录API调用失败:', error) + } + } + + clearLoginInfo() +} + +/** + * 检查并恢复登录状态 + * 用于小程序启动时调用 + * @returns {Promise<{isLoggedIn: boolean, user?: object}>} + */ +async function checkAndRestoreLogin() { + // 1. 先检查本地状态 + if (!isLoggedIn()) { + return { isLoggedIn: false } + } + + // 2. 验证服务端状态 + const result = await verifyLogin() + + if (result.valid) { + return { isLoggedIn: true, user: result.user } + } + + // 3. 验证失败,清除本地状态 + if (result.expired) { + console.log('Token已过期,需要重新登录') + } + + return { isLoggedIn: false } +} + +/** + * 需要登录时的处理 + * 跳转到登录页面 + * @param {string} redirectUrl - 登录成功后跳转的URL(可选) + */ +function requireLogin(redirectUrl = '') { + const url = redirectUrl + ? `/pages/login/login?redirect=${encodeURIComponent(redirectUrl)}` + : '/pages/login/login' + + wx.navigateTo({ + url, + fail: () => { + // 如果navigateTo失败,尝试redirectTo + wx.redirectTo({ url }) + } + }) +} + +/** + * 显示登录提示弹窗 + * @param {string} redirectUrl - 登录成功后跳转的URL(可选) + * @returns {Promise} 用户是否选择去登录 + */ +function showLoginTip(redirectUrl = '') { + return new Promise((resolve) => { + wx.showModal({ + title: '提示', + content: '请先登录后再操作', + confirmText: '去登录', + confirmColor: '#b06ab3', + success: (res) => { + if (res.confirm) { + requireLogin(redirectUrl) + resolve(true) + } else { + resolve(false) + } + } + }) + }) +} + +/** + * 页面级登录验证(统一验证机制) + * 用于页面onLoad时调用,确保用户已登录且token有效 + * + * @param {object} options - 配置选项 + * @param {string} options.pageName - 页面名称(用于日志) + * @param {string} options.redirectUrl - 未登录时的跳转URL + * @param {boolean} options.silent - 是否静默失败(不显示提示) + * @returns {Promise} 是否已登录且token有效 + * + * @example + * // 在页面onLoad中使用 + * async onLoad() { + * const isValid = await auth.ensureLogin({ + * pageName: 'gift-shop', + * redirectUrl: '/pages/gift-shop/gift-shop' + * }) + * + * if (!isValid) return + * + * // 继续加载页面数据 + * this.loadData() + * } + */ +async function ensureLogin(options = {}) { + const { + pageName = 'unknown', + redirectUrl = '', + silent = false + } = options + + const config = require('../config/index') + const app = getApp() + + console.log(`[auth.ensureLogin] ${pageName} - 开始验证登录状态`) + + // 1. 等待app初始化完成 + if (app && app.waitForLoginCheck) { + await app.waitForLoginCheck() + console.log(`[auth.ensureLogin] ${pageName} - app登录检查完成`) + } + + // 2. 检查全局登录状态 + if (!app || !app.globalData || !app.globalData.isLoggedIn) { + console.warn(`[auth.ensureLogin] ${pageName} - 全局状态显示未登录`) + if (!silent) { + wx.redirectTo({ + url: `/pages/login/login?redirect=${encodeURIComponent(redirectUrl)}` + }) + } + return false + } + + // 3. 检查token是否存在(最多重试3次) + let token = null + let retryCount = 0 + const maxRetries = 3 + + while (!token && retryCount < maxRetries) { + token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) + + if (!token) { + retryCount++ + console.warn(`[auth.ensureLogin] ${pageName} - Token不存在,第${retryCount}次重试`) + + if (retryCount < maxRetries) { + // 等待递增的时间:100ms, 200ms, 300ms + await new Promise(resolve => setTimeout(resolve, retryCount * 100)) + } + } + } + + if (!token) { + console.error(`[auth.ensureLogin] ${pageName} - Token在${maxRetries}次重试后仍然不存在`) + if (!silent) { + wx.redirectTo({ + url: `/pages/login/login?redirect=${encodeURIComponent(redirectUrl)}` + }) + } + return false + } + + // 4. 检查token是否过期 + const expiry = wx.getStorageSync(config.STORAGE_KEYS.TOKEN_EXPIRY) + if (expiry && new Date(expiry) <= new Date()) { + console.warn(`[auth.ensureLogin] ${pageName} - Token已过期`) + clearLoginInfo() + if (app && app.globalData) { + app.globalData.isLoggedIn = false + } + if (!silent) { + wx.redirectTo({ + url: `/pages/login/login?redirect=${encodeURIComponent(redirectUrl)}` + }) + } + return false + } + + // 5. 最终验证:再次确认token可以被获取(防止storage异步问题) + await new Promise(resolve => setTimeout(resolve, 50)) + const finalToken = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) + if (!finalToken) { + console.error(`[auth.ensureLogin] ${pageName} - 最终验证失败,token无法获取`) + if (!silent) { + wx.redirectTo({ + url: `/pages/login/login?redirect=${encodeURIComponent(redirectUrl)}` + }) + } + return false + } + + console.log(`[auth.ensureLogin] ${pageName} - 验证通过,token有效 (长度: ${finalToken.length})`) + return true +} + +module.exports = { + // 状态检查 + isLoggedIn, + getLocalUserInfo, + getToken, + getUserId, + + // 登录验证 + verifyLogin, + checkAndRestoreLogin, + ensureLogin, // 新增:统一的页面级登录验证 + + // 登录操作 + wxPhoneLogin, + logout, + + // 数据管理 + saveUserInfo, + clearLoginInfo, + + // 推荐码绑定 + checkAndBindReferralCode, + + // 辅助方法 + requireLogin, + showLoginTip +} diff --git a/utils/errorHandler.js b/utils/errorHandler.js new file mode 100644 index 0000000..b38552b --- /dev/null +++ b/utils/errorHandler.js @@ -0,0 +1,237 @@ +/** + * 统一错误处理工具 + * 提供统一的错误提示和处理机制 + */ + +/** + * 错误类型映射 + */ +const ERROR_TYPES = { + NETWORK: 'network', + AUTH: 'auth', + BUSINESS: 'business', + UNKNOWN: 'unknown' +} + +/** + * 错误消息映射 + */ +const ERROR_MESSAGES = { + // 网络错误 + 'request:fail': '网络连接失败,请检查网络设置', + 'request:fail timeout': '请求超时,请稍后重试', + 'request:fail abort': '请求已取消', + + // 认证错误 + 401: '登录已过期,请重新登录', + 403: '没有权限访问', + + // 业务错误 + 400: '请求参数错误', + 404: '请求的资源不存在', + 500: '服务器错误,请稍后重试', + + // 默认错误 + default: '操作失败,请稍后重试' +} + +/** + * 判断错误类型 + * @param {object} error - 错误对象 + * @returns {string} 错误类型 + */ +function getErrorType(error) { + if (!error) return ERROR_TYPES.UNKNOWN + + // 网络错误 + if (error.errMsg && error.errMsg.includes('request:fail')) { + return ERROR_TYPES.NETWORK + } + + // 认证错误 + if (error.code === 401 || error.statusCode === 401) { + return ERROR_TYPES.AUTH + } + + // 业务错误 + if (error.code || error.statusCode) { + return ERROR_TYPES.BUSINESS + } + + return ERROR_TYPES.UNKNOWN +} + +/** + * 获取错误消息 + * @param {object} error - 错误对象 + * @returns {string} 错误消息 + */ +function getErrorMessage(error) { + if (!error) return ERROR_MESSAGES.default + + // 优先使用后端返回的错误消息 + if (error.message) return error.message + if (error.error) return error.error + if (error.msg) return error.msg + + // 根据错误码获取消息 + const code = error.code || error.statusCode + if (code && ERROR_MESSAGES[code]) { + return ERROR_MESSAGES[code] + } + + // 根据错误信息获取消息 + if (error.errMsg && ERROR_MESSAGES[error.errMsg]) { + return ERROR_MESSAGES[error.errMsg] + } + + return ERROR_MESSAGES.default +} + +/** + * 显示错误提示 + * @param {object} error - 错误对象 + * @param {object} options - 选项 + */ +function showError(error, options = {}) { + const { + silent = false, // 是否静默(不显示提示) + duration = 2000, // 提示持续时间 + icon = 'none', // 图标类型 + mask = false // 是否显示透明蒙层 + } = options + + if (silent) return + + const message = getErrorMessage(error) + const type = getErrorType(error) + + // 认证错误不显示toast,由auth模块处理 + if (type === ERROR_TYPES.AUTH) { + return + } + + wx.showToast({ + title: message, + icon, + duration, + mask + }) +} + +/** + * 处理API错误 + * @param {object} error - 错误对象 + * @param {object} options - 选项 + * @returns {object} 处理后的错误对象 + */ +function handleApiError(error, options = {}) { + const { + showToast = true, // 是否显示错误提示 + logError = true, // 是否记录错误日志 + customMessage = null // 自定义错误消息 + } = options + + // 记录错误日志 + if (logError) { + console.error('[Error Handler]', error) + } + + // 显示错误提示 + if (showToast) { + showError(error, { + silent: false, + duration: 2000 + }) + } + + // 返回标准化的错误对象 + return { + type: getErrorType(error), + message: customMessage || getErrorMessage(error), + code: error.code || error.statusCode || -1, + originalError: error + } +} + +/** + * 错误重试机制 + * @param {function} fn - 要重试的函数 + * @param {object} options - 选项 + * @returns {Promise} + */ +async function retryOnError(fn, options = {}) { + const { + maxRetries = 3, // 最大重试次数 + retryDelay = 1000, // 重试延迟(毫秒) + onRetry = null // 重试回调 + } = options + + let lastError = null + + for (let i = 0; i < maxRetries; i++) { + try { + return await fn() + } catch (error) { + lastError = error + + // 认证错误不重试 + if (getErrorType(error) === ERROR_TYPES.AUTH) { + throw error + } + + // 最后一次重试失败,抛出错误 + if (i === maxRetries - 1) { + throw error + } + + // 执行重试回调 + if (onRetry) { + onRetry(i + 1, maxRetries) + } + + // 等待后重试 + await new Promise(resolve => setTimeout(resolve, retryDelay)) + } + } + + throw lastError +} + +/** + * 检查网络状态 + * @returns {Promise} + */ +function checkNetworkStatus() { + return new Promise((resolve) => { + wx.getNetworkType({ + success: (res) => { + const networkType = res.networkType + if (networkType === 'none') { + wx.showToast({ + title: '网络未连接', + icon: 'none', + duration: 2000 + }) + resolve(false) + } else { + resolve(true) + } + }, + fail: () => { + resolve(true) // 获取失败时假设网络正常 + } + }) + }) +} + +module.exports = { + ERROR_TYPES, + ERROR_MESSAGES, + getErrorType, + getErrorMessage, + showError, + handleApiError, + retryOnError, + checkNetworkStatus +} diff --git a/utils/imageUrl.js b/utils/imageUrl.js new file mode 100644 index 0000000..1d9c724 --- /dev/null +++ b/utils/imageUrl.js @@ -0,0 +1,121 @@ +/** + * 图片URL处理工具 + * 统一处理相对路径和绝对路径的图片URL + */ + +const config = require('../config/index') + +function getImageBaseUrl() { + const apiBaseUrl = String(config?.API_BASE_URL || '').trim() + if (!apiBaseUrl) return 'https://ai-c.maimanji.com' + try { + const parsed = new URL(apiBaseUrl) + return parsed.origin + } catch (_) { + return apiBaseUrl.replace(/\/api\/?$/, '').replace(/\/+$/, '') + } +} + +// 获取图片基础URL(只保留 origin) +const IMAGE_BASE_URL = getImageBaseUrl() + +/** + * 转换图片URL为完整地址 + * @param {string} url - 图片URL(可能是相对路径或绝对路径) + * @param {string} defaultImage - 默认图片路径(可选) + * @returns {string} 完整的图片URL + */ +function getFullImageUrl(url, defaultImage = '') { + // 如果没有URL,返回默认图片 + if (!url) { + return defaultImage || '/images/icon-empty.png' + } + + if (url.startsWith('wxfile://')) { + return defaultImage || '/images/icon-empty.png' + } + + if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('data:')) { + try { + const parsed = new URL(url) + if (parsed.pathname.startsWith('/uploads/')) { + parsed.pathname = `/api/uploads/${parsed.pathname.slice('/uploads/'.length)}` + return parsed.toString() + } + if (/^\/(avatars|characters|audio|documents|assets|interest-partners|exchange|products|temp)\//.test(parsed.pathname)) { + parsed.pathname = `/api/uploads${parsed.pathname}` + return parsed.toString() + } + return url + } catch (_) { + return url + } + } + + // 如果是本地图片路径(/images/开头),直接返回 + if (url.startsWith('/images/')) { + return url + } + + let processedUrl = url + if (processedUrl.startsWith('/uploads/')) { + processedUrl = `/api/uploads/${processedUrl.slice('/uploads/'.length)}` + } else if (processedUrl.startsWith('uploads/')) { + processedUrl = `/api/uploads/${processedUrl.slice('uploads/'.length)}` + } else if (/^(\/)?(avatars|characters|audio|documents|assets|interest-partners|exchange|products|temp)\//.test(processedUrl)) { + processedUrl = processedUrl.startsWith('/') ? `/api/uploads${processedUrl}` : `/api/uploads/${processedUrl}` + } + + // 其他相对路径,拼接服务器地址 + // 确保URL以/开头 + const normalizedUrl = processedUrl.startsWith('/') ? processedUrl : '/' + processedUrl + return IMAGE_BASE_URL + normalizedUrl +} + +/** + * 批量转换图片URL数组 + * @param {Array} urls - 图片URL数组 + * @returns {Array} 完整的图片URL数组 + */ +function getFullImageUrls(urls) { + if (!Array.isArray(urls)) { + return [] + } + return urls.map(url => getFullImageUrl(url)) +} + +/** + * 处理用户头像URL + * @param {string} avatar - 头像URL + * @returns {string} 完整的头像URL + */ +function getAvatarUrl(avatar) { + return getFullImageUrl(avatar, '/images/default-avatar.svg') +} + +/** + * 处理角色头像URL + * @param {string} avatar - 角色头像URL + * @returns {string} 完整的头像URL + */ +function getCharacterAvatarUrl(avatar) { + return getFullImageUrl(avatar, '/images/character-default.png') +} + +/** + * 处理活动封面URL + * @param {string} cover - 封面URL + * @returns {string} 完整的封面URL + */ +function getActivityCoverUrl(cover) { + return getFullImageUrl(cover, '/images/activity-default.jpg') +} + +module.exports = { + getFullImageUrl, + getFullImageUrls, + getAvatarUrl, + getCharacterAvatarUrl, + getActivityCoverUrl, + IMAGE_BASE_URL +} diff --git a/utils/payment.js b/utils/payment.js new file mode 100644 index 0000000..dd2822d --- /dev/null +++ b/utils/payment.js @@ -0,0 +1,321 @@ +/** + * 微信支付工具类 + * 封装微信支付相关功能 + */ + +const api = require('./api') +const config = require('../config/index') + +/** + * 发起微信支付 + * @param {string} orderId - 订单ID + * @param {string} orderType - 订单类型: recharge/vip/companion/gift + * @returns {Promise} + */ +const requestPayment = async (orderId, orderType = 'recharge') => { + try { + console.log('[Payment] ========== 开始支付流程 ==========') + console.log('[Payment] 订单ID:', orderId) + console.log('[Payment] 订单类型:', orderType) + + // 1. 调用后端API获取支付参数 + wx.showLoading({ title: '正在调起支付...', mask: true }) + + const paymentParams = await api.payment.prepay({ + orderId, + orderType + }) + + console.log('[Payment] 获取支付参数成功') + console.log('[Payment] 完整响应:', JSON.stringify(paymentParams)) + + // 后端返回格式: { success: true, data: { timeStamp, nonceStr, package, signType, paySign, ... } } + if (!paymentParams.success || !paymentParams.data) { + throw new Error(paymentParams.error || paymentParams.message || '获取支付参数失败') + } + + // 只提取微信支付需要的5个参数,忽略其他字段(如 total_fee, orderId 等) + const { + timeStamp, + nonceStr, + package: packageValue, + signType, + paySign, + // 以下字段仅用于日志,不传递给 wx.requestPayment + total_fee, + orderId: responseOrderId, + orderNo + } = paymentParams.data + + console.log('[Payment] 支付参数解析:') + console.log(' - timeStamp:', timeStamp) + console.log(' - nonceStr:', nonceStr) + console.log(' - package:', packageValue) + console.log(' - signType:', signType) + console.log(' - paySign:', paySign ? paySign.substring(0, 20) + '...' : 'null') + console.log(' - total_fee (仅日志):', total_fee) + console.log(' - orderId (仅日志):', responseOrderId) + console.log(' - orderNo (仅日志):', orderNo) + + // 验证必要参数 + if (!timeStamp || !nonceStr || !packageValue || !paySign) { + console.error('[Payment] 支付参数不完整') + throw new Error('支付参数不完整') + } + + // 2. 调用微信支付API + wx.hideLoading() + + console.log('[Payment] 调用wx.requestPayment') + console.log('[Payment] ⚠️ 注意:只传递5个必需参数,不传递 total_fee') + + // 构造支付参数对象,确保只包含5个必需字段 + const paymentRequest = { + timeStamp: String(timeStamp), + nonceStr: String(nonceStr), + package: String(packageValue), + signType: signType || 'MD5', + paySign: String(paySign) + } + + console.log('[Payment] 最终支付参数:', JSON.stringify(paymentRequest)) + + return new Promise((resolve, reject) => { + wx.requestPayment({ + ...paymentRequest, + success: (res) => { + console.log('[Payment] ✓ 支付成功:', res) + resolve({ success: true, message: '支付成功' }) + }, + fail: (err) => { + console.error('[Payment] ✗ 支付失败:', err) + console.error('[Payment] 错误详情:', JSON.stringify(err)) + + // 用户取消支付 + if (err.errMsg === 'requestPayment:fail cancel') { + reject({ code: 'USER_CANCEL', message: '您已取消支付' }) + } + // 支付失败 + else { + reject({ + code: 'PAYMENT_FAIL', + message: err.errMsg || '支付失败,请稍后重试', + detail: err + }) + } + } + }) + }) + } catch (error) { + wx.hideLoading() + console.error('[Payment] ✗ 支付流程错误:', error) + throw error + } +} + +/** + * 查询订单支付状态 + * @param {string} orderId - 订单ID + * @param {number} confirm - 是否主动确认 (1/0) + * @returns {Promise} + */ +const queryOrderStatus = async (orderId, confirm = 0) => { + try { + console.log('[Payment] 查询订单状态:', orderId, 'confirm:', confirm) + const res = await api.payment.queryOrder(orderId, { confirm }) + console.log('[Payment] 订单状态:', res.data?.status) + return res + } catch (error) { + console.error('[Payment] 查询订单状态失败:', error) + throw error + } +} + +/** + * 轮询查询订单状态 + * @param {string} orderId - 订单ID + * @param {number} maxRetries - 最大重试次数 + * @param {number} interval - 轮询间隔(ms) + * @returns {Promise} + */ +const pollOrderStatus = (orderId, maxRetries = 30, interval = 2000) => { + return new Promise((resolve, reject) => { + let retries = 0 + + console.log('[Payment] 开始轮询订单状态') + console.log('[Payment] 最大重试次数:', maxRetries) + console.log('[Payment] 轮询间隔:', interval, 'ms') + + const poll = async () => { + try { + retries++ + console.log(`[Payment] 第 ${retries}/${maxRetries} 次查询`) + + // 前3次查询带 confirm=1 参数,促使后端主动向微信查询状态 + const confirm = retries <= 3 ? 1 : 0 + const res = await queryOrderStatus(orderId, confirm) + + if (res.success && res.data) { + const status = res.data.status + + console.log(`[Payment] 订单状态: ${status}`) + + // 支付成功 - 后端状态: completed + if (status === 'completed') { + console.log('[Payment] ✓ 订单支付成功并已完成') + resolve({ success: true, data: res.data, status: 'completed' }) + return + } + + // 已支付但未完成 - 后端状态: paid + if (status === 'paid') { + console.log('[Payment] ✓ 订单已支付,等待完成') + // 继续轮询,等待变为 completed + } + + // 支付失败 - 后端状态: cancelled + if (status === 'cancelled') { + console.log('[Payment] ✗ 订单已取消') + reject({ code: 'ORDER_CANCELLED', message: '订单已取消' }) + return + } + + // 继续轮询 + if (retries < maxRetries) { + console.log('[Payment] 订单状态为', status, ',继续轮询...') + setTimeout(poll, interval) + } else { + console.log('[Payment] ✗ 查询超时') + reject({ + code: 'TIMEOUT', + message: '支付结果查询超时,请稍后在订单列表中查看', + data: res.data + }) + } + } else { + reject({ code: 'QUERY_FAIL', message: res.error || res.message || '查询订单失败' }) + } + } catch (error) { + console.error('[Payment] 轮询查询失败:', error) + + // 如果是网络错误,继续重试 + if (retries < maxRetries) { + console.log('[Payment] 查询出错,继续重试...') + setTimeout(poll, interval) + } else { + reject(error) + } + } + } + + // 开始第一次查询 + poll() + }) +} + +/** + * 完整支付流程(创建订单 + 支付 + 查询状态) + * @param {object} orderData - 订单数据 + * @param {string} orderType - 订单类型 + * @returns {Promise} + */ +const completePayment = async (orderData, orderType) => { + try { + console.log('[Payment] ========== 完整支付流程开始 ==========') + console.log('[Payment] 测试模式:', config.TEST_MODE ? '开启' : '关闭') + + // 1. 创建订单 + wx.showLoading({ title: '创建订单中...', mask: true }) + + let orderRes + switch (orderType) { + case 'recharge': + orderRes = await api.payment.createRechargeOrder(orderData) + break + case 'vip': + orderRes = await api.payment.createVipOrder(orderData) + break + case 'companion': + orderRes = await api.companion.createOrder(orderData) + break + default: + throw new Error('不支持的订单类型') + } + + if (!orderRes.success || !orderRes.data) { + throw new Error(orderRes.message || '创建订单失败') + } + + const orderId = orderRes.data.orderId + console.log('[Payment] ✓ 订单创建成功:', orderId) + + wx.hideLoading() + + // 测试模式:跳过微信支付,直接模拟支付成功 + if (config.TEST_MODE) { + console.log('[Payment] ⚠️ 测试模式:跳过微信支付,直接模拟支付成功') + + wx.showLoading({ title: '模拟支付中...', mask: true }) + + // 延迟1秒模拟支付过程 + await new Promise(resolve => setTimeout(resolve, 1000)) + + // 调用后端测试支付接口,直接标记订单为已支付 + try { + const testPayRes = await api.payment.testPay({ orderId }) + console.log('[Payment] ✓ 测试支付成功:', testPayRes) + } catch (error) { + console.error('[Payment] ✗ 测试支付失败:', error) + // 即使测试支付接口失败,也继续流程(可能后端没有此接口) + } + + wx.hideLoading() + + // 显示成功提示 + wx.showToast({ + title: '支付成功(测试)', + icon: 'success', + duration: 2000 + }) + + console.log('[Payment] ========== 支付流程完成(测试模式)==========') + + return { + success: true, + orderId, + testMode: true, + message: '测试模式支付成功' + } + } + + // 正式模式:走真实微信支付流程 + // 2. 发起支付 + await requestPayment(orderId, orderType) + + // 3. 查询订单状态 + wx.showLoading({ title: '支付成功,处理中...', mask: true }) + + const statusRes = await pollOrderStatus(orderId) + + wx.hideLoading() + + console.log('[Payment] ========== 支付流程完成 ==========') + + return { + success: true, + orderId, + orderData: statusRes.data + } + } catch (error) { + wx.hideLoading() + console.error('[Payment] ========== 支付流程失败 ==========') + throw error + } +} + +module.exports = { + requestPayment, + queryOrderStatus, + pollOrderStatus, + completePayment +} diff --git a/utils/proactiveMessage.js b/utils/proactiveMessage.js new file mode 100644 index 0000000..3ecc281 --- /dev/null +++ b/utils/proactiveMessage.js @@ -0,0 +1,270 @@ +/** + * AI角色主动推送消息工具模块 + * + * 功能说明: + * 1. 跟进消息:发送给已聊过天但一段时间没互动的用户 + * 2. 打招呼消息:发送给还没有和任何角色聊过天的新用户 + */ + +const api = require('./api') +const config = require('../config/index') + +// 上次检查时间(避免频繁请求) +let lastCheckTime = 0 +// 检查间隔(毫秒)- 默认5分钟 +const CHECK_INTERVAL = 5 * 60 * 1000 + +/** + * 获取待接收的主动推送消息 + * @returns {Promise} 消息列表 + */ +async function getPendingMessages() { + try { + const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) + if (!token) { + console.log('[proactiveMessage] 未登录,跳过获取推送消息') + return [] + } + + console.log('[proactiveMessage] 开始获取待推送消息...') + const res = await api.proactiveMessage.getPending() + console.log('[proactiveMessage] API响应:', JSON.stringify(res)) + + if (res.success && res.data) { + console.log('[proactiveMessage] 获取到消息数量:', res.data.length) + return res.data || [] + } + return [] + } catch (error) { + console.error('[proactiveMessage] 获取推送消息失败:', error) + return [] + } +} + +/** + * 检查并处理推送消息 + * 在首页 onShow 或定时调用 + * @param {object} options - 配置选项 + * @param {boolean} options.force - 是否强制检查(忽略时间间隔) + * @param {function} options.onNewMessages - 收到新消息时的回调 + * @returns {Promise} 消息列表 + */ +async function checkAndShowMessages(options = {}) { + const { force = false, onNewMessages } = options + + // 检查登录状态 + const app = getApp() + if (!app || !app.globalData || !app.globalData.isLoggedIn) { + console.log('[proactiveMessage] 未登录,跳过检查') + return [] + } + + // 检查时间间隔(避免频繁请求) + const now = Date.now() + if (!force && lastCheckTime && (now - lastCheckTime < CHECK_INTERVAL)) { + console.log('[proactiveMessage] 距离上次检查不足5分钟,跳过。上次:', lastCheckTime, '现在:', now) + return [] + } + + console.log('[proactiveMessage] 开始检查推送消息,force:', force) + lastCheckTime = now + + const messages = await getPendingMessages() + + if (messages.length > 0) { + console.log('[proactiveMessage] 收到推送消息:', messages.length, '条') + console.log('[proactiveMessage] 消息详情:', JSON.stringify(messages)) + + // 更新会话列表的未读状态 + messages.forEach(msg => { + updateConversationUnread(msg) + }) + + // 触发回调 + if (typeof onNewMessages === 'function') { + onNewMessages(messages) + } + } else { + console.log('[proactiveMessage] 没有待推送消息') + } + + return messages +} + +/** + * 更新会话列表未读状态 + * @param {object} msg - 推送消息对象 + */ +function updateConversationUnread(msg) { + // 获取当前会话列表缓存 + const cacheKey = 'proactive_messages_cache' + let cache = wx.getStorageSync(cacheKey) || {} + + // 记录消息,避免重复处理 + const msgKey = `${msg.character_id}_${msg.sent_at}` + if (cache[msgKey]) { + return // 已处理过 + } + + cache[msgKey] = { + character_id: msg.character_id, + character_name: msg.character_name, + content: msg.content, + message_type: msg.message_type, + sent_at: msg.sent_at, + processed_at: new Date().toISOString() + } + + // 清理过期缓存(保留最近24小时的记录) + const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000 + Object.keys(cache).forEach(key => { + const item = cache[key] + if (new Date(item.processed_at).getTime() < oneDayAgo) { + delete cache[key] + } + }) + + wx.setStorageSync(cacheKey, cache) + + // 触发页面更新 + triggerPageRefresh() +} + +/** + * 触发页面刷新 + * 通知当前页面刷新会话列表 + */ +function triggerPageRefresh() { + const pages = getCurrentPages() + if (pages.length === 0) return + + const currentPage = pages[pages.length - 1] + + // 如果当前页面有刷新会话列表的方法,调用它 + if (currentPage && typeof currentPage.loadConversations === 'function') { + currentPage.loadConversations() + } + + // 如果当前页面有刷新未读数的方法,调用它 + if (currentPage && typeof currentPage.loadUnreadCount === 'function') { + currentPage.loadUnreadCount() + } +} + +/** + * 获取角色的未处理推送消息 + * @param {string} characterId - 角色ID + * @returns {object|null} 消息对象 + */ +function getCharacterPendingMessage(characterId) { + const cacheKey = 'proactive_messages_cache' + const cache = wx.getStorageSync(cacheKey) || {} + + // 查找该角色的最新消息 + let latestMsg = null + Object.values(cache).forEach(msg => { + if (msg.character_id === characterId) { + if (!latestMsg || new Date(msg.sent_at) > new Date(latestMsg.sent_at)) { + latestMsg = msg + } + } + }) + + return latestMsg +} + +/** + * 清除角色的推送消息缓存 + * 用户进入聊天后调用 + * @param {string} characterId - 角色ID + */ +function clearCharacterMessages(characterId) { + const cacheKey = 'proactive_messages_cache' + let cache = wx.getStorageSync(cacheKey) || {} + + // 删除该角色的所有消息 + Object.keys(cache).forEach(key => { + if (cache[key].character_id === characterId) { + delete cache[key] + } + }) + + wx.setStorageSync(cacheKey, cache) +} + +/** + * 标记角色的推送消息为已读 + * 调用后端API标记已读,同时清除本地缓存 + * @param {string} characterId - 角色ID + */ +async function markAsRead(characterId) { + if (!characterId) { + console.log('[proactiveMessage] 没有角色ID,跳过标记已读') + return + } + + // 先清除本地缓存 + clearCharacterMessages(characterId) + + // 调用后端API标记已读 + try { + const res = await api.proactiveMessage.markAsRead({ + character_id: characterId + }) + console.log('[proactiveMessage] 标记已读结果:', JSON.stringify(res)) + } catch (err) { + console.log('[proactiveMessage] 标记已读失败:', err) + } +} + +/** + * 按消息ID列表标记已读 + * @param {Array} messageIds - 消息ID列表 + */ +async function markAsReadByIds(messageIds) { + if (!messageIds || messageIds.length === 0) { + return + } + + try { + const res = await api.proactiveMessage.markAsRead({ + message_ids: messageIds + }) + console.log('[proactiveMessage] 按ID标记已读结果:', JSON.stringify(res)) + } catch (err) { + console.log('[proactiveMessage] 按ID标记已读失败:', err) + } +} + +/** + * 启动定时检查 + * @param {number} interval - 检查间隔(毫秒),默认5分钟 + * @returns {number} 定时器ID + */ +function startPeriodicCheck(interval = CHECK_INTERVAL) { + return setInterval(() => { + checkAndShowMessages() + }, interval) +} + +/** + * 停止定时检查 + * @param {number} timerId - 定时器ID + */ +function stopPeriodicCheck(timerId) { + if (timerId) { + clearInterval(timerId) + } +} + +module.exports = { + getPendingMessages, + checkAndShowMessages, + getCharacterPendingMessage, + clearCharacterMessages, + markAsRead, + markAsReadByIds, + startPeriodicCheck, + stopPeriodicCheck, + CHECK_INTERVAL +} diff --git a/utils/util.js b/utils/util.js new file mode 100644 index 0000000..b69c843 --- /dev/null +++ b/utils/util.js @@ -0,0 +1,317 @@ +/** + * 工具函数 + */ + +const config = require('../config/index') + +/** + * 格式化时间 + * @param {Date|string|number} date - 日期 + * @param {string} format - 格式 (默认 'YYYY-MM-DD HH:mm:ss') + */ +const formatTime = (date, format = 'YYYY-MM-DD HH:mm:ss') => { + if (!date) return '' + + const d = new Date(date) + if (isNaN(d.getTime())) return '' + + const year = d.getFullYear() + const month = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + const hour = String(d.getHours()).padStart(2, '0') + const minute = String(d.getMinutes()).padStart(2, '0') + const second = String(d.getSeconds()).padStart(2, '0') + + return format + .replace('YYYY', year) + .replace('MM', month) + .replace('DD', day) + .replace('HH', hour) + .replace('mm', minute) + .replace('ss', second) +} + +/** + * 格式化相对时间 + * @param {Date|string|number} date - 日期 + */ +const formatRelativeTime = (date) => { + if (!date) return '' + + const d = new Date(date) + if (isNaN(d.getTime())) return '' + + const now = new Date() + const diff = now.getTime() - d.getTime() + const seconds = Math.floor(diff / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + const days = Math.floor(hours / 24) + + if (seconds < 60) return '刚刚' + if (minutes < 60) return `${minutes}分钟前` + if (hours < 24) return `${hours}小时前` + if (days < 7) return `${days}天前` + if (days < 30) return `${Math.floor(days / 7)}周前` + + // 超过30天显示具体日期 + return formatTime(d, 'MM-DD HH:mm') +} + +/** + * 格式化数字(添加千分位) + * @param {number} num - 数字 + */ +const formatNumber = (num) => { + if (num === null || num === undefined) return '0' + return Number(num).toLocaleString() +} + +/** + * 格式化金额 + * @param {number} amount - 金额(分) + * @param {boolean} showSymbol - 是否显示符号 + */ +const formatMoney = (amount, showSymbol = true) => { + if (amount === null || amount === undefined) return showSymbol ? '¥0.00' : '0.00' + const yuan = (amount / 100).toFixed(2) + return showSymbol ? `¥${yuan}` : yuan +} + +/** + * 手机号脱敏 + * @param {string} phone - 手机号 + */ +const maskPhone = (phone) => { + if (!phone || phone.length < 11) return phone + return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') +} + +/** + * 检查是否登录 + */ +const isLoggedIn = () => { + const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) + return !!token +} + +/** + * 获取存储的用户信息 + */ +const getUserInfo = () => { + return wx.getStorageSync(config.STORAGE_KEYS.USER_INFO) || null +} + +/** + * 保存用户信息 + * @param {object} userInfo - 用户信息 + */ +const saveUserInfo = (userInfo) => { + wx.setStorageSync(config.STORAGE_KEYS.USER_INFO, userInfo) + if (userInfo && userInfo.id) { + wx.setStorageSync(config.STORAGE_KEYS.USER_ID, userInfo.id) + } +} + +/** + * 保存登录Token + * @param {string} token - Token + */ +const saveToken = (token) => { + wx.setStorageSync(config.STORAGE_KEYS.TOKEN, token) +} + +/** + * 清除登录状态 + */ +const clearAuth = () => { + wx.removeStorageSync(config.STORAGE_KEYS.TOKEN) + wx.removeStorageSync(config.STORAGE_KEYS.REFRESH_TOKEN) + wx.removeStorageSync(config.STORAGE_KEYS.USER_INFO) + wx.removeStorageSync(config.STORAGE_KEYS.USER_ID) +} + +/** + * 显示加载中 + * @param {string} title - 提示文字 + */ +const showLoading = (title = '加载中...') => { + wx.showLoading({ title, mask: true }) +} + +/** + * 隐藏加载中 + */ +const hideLoading = () => { + wx.hideLoading() +} + +/** + * 显示成功提示 + * @param {string} title - 提示文字 + */ +const showSuccess = (title) => { + wx.showToast({ title, icon: 'success' }) +} + +/** + * 显示错误提示 + * @param {string} title - 提示文字 + */ +const showError = (title) => { + wx.showToast({ title, icon: 'none' }) +} + +/** + * 显示普通提示 + * @param {string} title - 提示文字 + */ +const showToast = (title) => { + wx.showToast({ title, icon: 'none' }) +} + +/** + * 显示确认弹窗 + * @param {object} options - 选项 + */ +const showConfirm = (options) => { + return new Promise((resolve) => { + wx.showModal({ + title: options.title || '提示', + content: options.content || '', + showCancel: options.showCancel !== false, + cancelText: options.cancelText || '取消', + confirmText: options.confirmText || '确定', + confirmColor: options.confirmColor || '#b06ab3', + success: (res) => { + resolve(res.confirm) + } + }) + }) +} + +/** + * 防抖函数 + * @param {Function} fn - 函数 + * @param {number} delay - 延迟时间 + */ +const debounce = (fn, delay = 300) => { + let timer = null + return function (...args) { + if (timer) clearTimeout(timer) + timer = setTimeout(() => { + fn.apply(this, args) + }, delay) + } +} + +/** + * 节流函数 + * @param {Function} fn - 函数 + * @param {number} interval - 间隔时间 + */ +const throttle = (fn, interval = 300) => { + let lastTime = 0 + return function (...args) { + const now = Date.now() + if (now - lastTime >= interval) { + lastTime = now + fn.apply(this, args) + } + } +} + +/** + * 深拷贝 + * @param {any} obj - 对象 + */ +const deepClone = (obj) => { + if (obj === null || typeof obj !== 'object') return obj + if (obj instanceof Date) return new Date(obj) + if (obj instanceof Array) return obj.map(item => deepClone(item)) + + const cloned = {} + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + cloned[key] = deepClone(obj[key]) + } + } + return cloned +} + +/** + * 生成唯一ID + */ +const generateId = () => { + return Date.now().toString(36) + Math.random().toString(36).substr(2, 9) +} + +/** + * 获取完整的图片URL + * @param {string} url - 原始URL + * @param {string} defaultImage - 默认图片 + */ +const getFullImageUrl = (url, defaultImage = '') => { + if (!url) return defaultImage || '' + + if (url.startsWith('wxfile://')) return defaultImage || '' + + // 如果已经是完整 URL 且不是 localhost, 直接返回 + if (url.startsWith('http') && !url.includes('localhost')) return url; + + // 如果是 localhost, 将其替换为正式域名 + if (url.includes('localhost')) { + const path = url.split(':3000')[1] || url.split('localhost')[1] + return `https://ai-c.maimanji.com${path.startsWith('/') ? '' : '/'}${path}` + } + + // 如果是 /images/ 开头的路径, 也需要拼接域名 + // 以前这里直接返回了 url, 导致微信小程序尝试加载本地资源而失败 + let baseUrl = 'https://ai-c.maimanji.com' + if (config && config.API_BASE_URL) { + try { + const parsed = new URL(String(config.API_BASE_URL)) + baseUrl = parsed.origin + } catch (_) { + baseUrl = String(config.API_BASE_URL).replace(/\/api\/?$/, '').replace(/\/+$/, '') + } + } + + let processedUrl = url + if (processedUrl.startsWith('/uploads/')) { + processedUrl = `/api/uploads/${processedUrl.slice('/uploads/'.length)}` + } else if (processedUrl.startsWith('uploads/')) { + processedUrl = `/api/uploads/${processedUrl.slice('uploads/'.length)}` + } else if (/^(\/)?(avatars|characters|audio|documents|assets|interest-partners|exchange|products|temp)\//.test(processedUrl)) { + processedUrl = processedUrl.startsWith('/') ? `/api/uploads${processedUrl}` : `/api/uploads/${processedUrl}` + } + + // 确保以 / 开头 + const normalizedUrl = processedUrl.startsWith('/') ? processedUrl : '/' + processedUrl + + return baseUrl + normalizedUrl +} + +module.exports = { + formatTime, + formatRelativeTime, + formatNumber, + formatMoney, + maskPhone, + isLoggedIn, + getUserInfo, + saveUserInfo, + saveToken, + clearAuth, + showLoading, + hideLoading, + showSuccess, + showError, + showToast, + showConfirm, + debounce, + throttle, + deepClone, + generateId, + getFullImageUrl +} diff --git a/utils_new/auth.js b/utils_new/auth.js new file mode 100644 index 0000000..aff590c --- /dev/null +++ b/utils_new/auth.js @@ -0,0 +1,51 @@ +const { request } = require('./request'); + +const login = async (userInfo) => { + const codeRes = await new Promise((resolve, reject) => { + wx.login({ + success: resolve, + fail: reject + }); + }); + + const code = codeRes.code; + const res = await request({ + url: '/api/auth/wx-login', + method: 'POST', + data: { code, userInfo: userInfo || null } + }); + + const body = res.data || {}; + if (!body.success) { + throw new Error(body.error || '登录失败'); + } + + const token = body.data?.token || ''; + if (!token) { + throw new Error('登录失败:缺少 token'); + } + + wx.setStorageSync('auth_token', token); + const app = getApp(); + if (app?.globalData) app.globalData.token = token; + return body.data?.user || null; +}; + +const fetchMe = async () => { + try { + const res = await request({ url: '/api/auth/me', method: 'GET' }); + const body = res.data || {}; + if (!body.success) throw new Error(body.message || '获取用户信息失败'); + return body.data; + } catch (e) { + const msg = e?.userMessage || e?.message || '获取用户信息失败'; + const err = new Error(msg); + err.baseUrl = e?.baseUrl; + throw err; + } +}; + +module.exports = { + login, + fetchMe +}; diff --git a/utils_new/payment.js b/utils_new/payment.js new file mode 100644 index 0000000..b09f4a7 --- /dev/null +++ b/utils_new/payment.js @@ -0,0 +1,93 @@ +const { request } = require('./request'); + +const createVipOrder = async ({ planId, duration }) => { + const res = await request({ + url: '/api/payment/vip', + method: 'POST', + data: { planId, duration, paymentMethod: 'wechat' } + }); + const body = res.data || {}; + if (!body.success) throw new Error(body.error || '创建VIP订单失败'); + return body.data; +}; + +const createProductOrder = async ({ productId, referralCode }) => { + const res = await request({ + url: `/api/products/${productId}/purchase`, + method: 'POST', + data: { payment_method: 'wechat', ...(referralCode ? { referral_code: referralCode } : {}) } + }); + const body = res.data || {}; + if (!body.success) throw new Error(body.error || '创建订单失败'); + const data = body.data || {}; + return { + orderId: data.order_id, + orderNo: data.order_no, + amount: data.amount, + productName: data.product_name, + expireAt: data.expire_at + }; +}; + +const getPrepayParams = async ({ orderId, orderType }) => { + const res = await request({ + url: '/api/payment/prepay', + method: 'POST', + data: { orderId, orderType } + }); + const body = res.data || {}; + if (!body.success) throw new Error(body.error || '获取支付参数失败'); + return body.data; +}; + +const requestPayment = async (params) => { + return new Promise((resolve, reject) => { + const paymentArgs = { + timeStamp: String(params.timeStamp), + nonceStr: params.nonceStr, + package: params.package, + signType: params.signType, + paySign: params.paySign, + // 部分老旧兼容可能需要 total_fee,尝试带上 + ...(params.total_fee ? { total_fee: params.total_fee } : {}) + }; + + console.log('[Payment] Requesting wx.requestPayment with:', JSON.stringify(paymentArgs)); + + wx.requestPayment({ + ...paymentArgs, + success: (res) => { + console.log('[Payment] Success:', res); + resolve(res); + }, + fail: (err) => { + console.error('[Payment] Fail:', err); + // 转换错误信息 + const msg = err.errMsg || ''; + if (msg.includes('parameter error') || msg.includes('missing parameter')) { + console.error('[Payment] Parameter Error Details:', paymentArgs); + } + reject(err); + } + }); + }); +}; + +const payVip = async ({ planId, duration }) => { + const order = await createVipOrder({ planId, duration }); + const prepay = await getPrepayParams({ orderId: order.orderId, orderType: 'vip' }); + await requestPayment(prepay); + return order; +}; + +const payProduct = async ({ productId, orderType, referralCode }) => { + const order = await createProductOrder({ productId, referralCode }); + const prepay = await getPrepayParams({ orderId: order.orderId, orderType }); + await requestPayment(prepay); + return order; +}; + +module.exports = { + payVip, + payProduct +}; diff --git a/utils_new/request.js b/utils_new/request.js new file mode 100644 index 0000000..c3392a3 --- /dev/null +++ b/utils_new/request.js @@ -0,0 +1,57 @@ +const getAppInstance = () => getApp(); +const config = require('../config/index'); + +const getDefaultBaseUrl = () => { + const apiBaseUrl = String(config?.API_BASE_URL || '').replace(/\/+$/, ''); + if (!apiBaseUrl) return 'https://ai-c.maimanji.com'; + return apiBaseUrl.endsWith('/api') ? apiBaseUrl.slice(0, -4) : apiBaseUrl; +}; + +const getBaseUrl = () => { + const app = getAppInstance(); + const fromGlobal = app?.globalData?.baseUrl; + const fromStorage = wx.getStorageSync('baseUrl'); + if (config?.ENV === 'development') { + const storageUrl = String(fromStorage || '').trim(); + const allowStorage = + /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i.test(storageUrl) || + /^https?:\/\/(0\.0\.0\.0)(:\d+)?$/i.test(storageUrl); + return (allowStorage ? storageUrl : '') || fromGlobal || getDefaultBaseUrl(); + } + return fromStorage || fromGlobal || getDefaultBaseUrl(); +}; + +const getToken = () => wx.getStorageSync('auth_token') || ''; + +const request = ({ url, method = 'GET', data, header = {} }) => { + return new Promise((resolve, reject) => { + const baseUrl = getBaseUrl(); + wx.request({ + url: `${baseUrl}${url}`, + method, + data, + header: { + 'Content-Type': 'application/json', + ...(getToken() ? { Authorization: `Bearer ${getToken()}` } : {}), + ...header + }, + success: (res) => { + resolve(res); + }, + fail: (err) => { + const errMsg = err?.errMsg || ''; + const isConnRefused = /ERR_CONNECTION_REFUSED/i.test(errMsg); + const isRequestFail = /^request:fail/i.test(errMsg); + const userMessage = isConnRefused || isRequestFail + ? `网络连接失败:无法连接到 ${baseUrl},请确认后端服务已启动且接口地址正确` + : '网络请求失败'; + reject({ ...err, baseUrl, userMessage }); + } + }); + }); +}; + +module.exports = { + request, + getBaseUrl +}; diff --git a/前端样式修复经验.md b/前端样式修复经验.md new file mode 100644 index 0000000..d506ea5 --- /dev/null +++ b/前端样式修复经验.md @@ -0,0 +1,113 @@ +# 前端样式修复经验(小程序为主) + +本文整理了在本项目里做“像素级对齐 Figma”时,高频踩坑与可复用修复策略,重点覆盖微信小程序(WXML/WXSS)场景。 + +## 1. 先判断:是样式问题还是“没生效” + +很多“样式不对”的根因不是写错了,而是没有加载到新代码: + +- **确认正在编辑的就是运行中的项目目录**:开发者工具「详情」里的项目路径必须指向当前仓库的 `miniprogram` 目录。 +- **清缓存再编译**:开发者工具经常缓存 WXSS,建议「清缓存(全部)」+ 重新编译。 +- **确认页面路径一致**:例如 `pages/cooperation/cooperation` 与 `pages/profile/cooperation/cooperation` 可能并存,确保你改的是当前路由实际打开的页面。 + +## 2. 小程序的“宽度为什么只居中一小块” + +在小程序里,`button` + `scroll-view` + `flex` 的组合经常出现“看起来只占中间一小块”的情况。 + +**典型症状** +- 卡片明明写了 `width: 100%`,但实际渲染仍像居中固定宽度。 +- 文本被挤到很窄,标题变成 `休...` 或出现逐字竖排。 + +**常见原因** +- 父容器未明确 `width: 100%`,或 `scroll-view` 的布局约束导致子项无法 stretch。 +- `button` 在某些布局里会表现为“内容宽度”,需要额外强制伸展。 +- 文本容器缺少 `min-width: 0`,导致 flex 收缩异常。 + +**可复用的修复模板** +- 父级容器明确宽度: + - `scroll-view`:`width: 100%` + - 外层容器:`width: 100%` +- 列表容器强制 stretch: + - `display: flex; flex-direction: column; align-items: stretch; width: 100%;` +- 按钮强制占满: + - `width: 100%; min-width: 100%; align-self: stretch; box-sizing: border-box;` +- 文本区域允许正确收缩: + - `min-width: 0;` + +## 3. 文本“逐字竖排/奇怪换行”的处理 + +**典型症状** +- 标题被拆成一列:`休 / 闲 / 娱 / 乐` +- 或者标题过早省略/换行,不符合设计稿 + +**处理策略** +- 让标题与副标题稳定单行: + - `display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;` +- 对于 flex 子项的文本容器,必须补: + - `min-width: 0;` + +## 4. scroll-view 下“底部被遮挡/露出不完整” + +**典型症状** +- 最底部按钮/卡片被 tabBar、底部安全区、或页面 padding 覆盖。 +- 看起来像是“卡片被裁掉一截”。 + +**根因** +- `.safe-bottom` 加在 page 上不等于加在 `scroll-view` 的可滚动内容上。 +- `scroll-view` 本身高度固定,但内容缺少足够的 `padding-bottom`。 + +**推荐修复** +- 直接给 `scroll-view` 增加安全区 padding: + +```css +.content { + padding-bottom: calc(constant(safe-area-inset-bottom) + 48rpx); + padding-bottom: calc(env(safe-area-inset-bottom) + 48rpx); +} +``` + +并给最后一个卡片额外 `margin-bottom`,确保视觉呼吸感与可点击区域安全。 + +## 5. gap 兼容性:能不用就不用 + +WXSS 对 `gap`(尤其是 `flex-gap`)在不同基础库/机型上可能出现不一致(或表现和浏览器不同)。 + +**建议** +- 列表纵向间距优先用: + - `.item + .item { margin-top: 32rpx; }` +- 行内间距用 margin(例如 `icon` 与文本之间 `margin-right`)。 + +## 6. button 默认样式与点击态 + +小程序 `button` 默认会带: +- 内边距/圆角/背景 +- `::after` 的边框 +- 不同平台的点击态 + +**建议** +- 统一使用 `.btn-reset`(项目已有全局定义)清除默认干扰: + - `padding:0; margin:0; background:transparent;` + - `.btn-reset::after { border:none; }` + +## 7. 像素级对齐的工作流(推荐) + +1. **先拿到 Figma 的“明确数值”**:宽高、圆角、阴影、字体、行高、padding/gap。 +2. **结构先对齐,再对齐视觉**:先搭 WXML 的盒子结构,保证布局稳定,再上阴影/渐变/图标。 +3. **把关键 UI 组件 token 化**:比如统一 `cardRadius/btnHeight/shadow`,避免每个页面都手写一套。 +4. **最小化依赖**:图标/插画尽量用导出的资源或现有 icon 组件,减少渲染差异。 +5. **用“对照图”验收**:左侧 Figma,右侧真机预览;优先修复影响布局的约束(宽度/对齐/滚动)。 + +## 8. 本项目中一次典型问题复盘(合作入驻页) + +**问题 1:入口卡片居中变窄** +- 原因:按钮在 scroll-view + flex 下未伸展 + 文本容器可用宽度被压缩。 +- 修复:父容器/列表容器/按钮三层都强制 `width:100%`,并给文本容器补 `min-width:0`。 + +**问题 2:底部“我的订单”卡片露出不完整** +- 原因:`scroll-view` 缺少安全区 padding-bottom。 +- 修复:给 `scroll-view.content` 增加 `padding-bottom: calc(env(safe-area-inset-bottom)+48rpx)`,并给最后卡片加 `margin-bottom`。 + +--- + +如果你希望把这套经验进一步沉淀成“组件级规范”(卡片/列表项/按钮/导航栏 token + 通用 mixin),我也可以基于当前项目结构继续抽象一套可复用的 UI 规范文档与组件模板。 + diff --git a/小程序前端开发与后端API对接经验汇总.md b/小程序前端开发与后端API对接经验汇总.md new file mode 100644 index 0000000..36000bd --- /dev/null +++ b/小程序前端开发与后端API对接经验汇总.md @@ -0,0 +1,219 @@ +# 小程序前端开发与后端 API 对接经验汇总 + +适用范围:本仓库小程序前端(`qianduan/qianduan-code/miniprogram`)。目标是让新同学能在不踩坑的情况下完成页面开发、样式对齐和后端 API 对接。 + +## 1. 项目结构与入口 + +- 小程序目录:`qianduan/qianduan-code/miniprogram` +- 全局入口:`app.js / app.json / app.wxss` +- 页面目录:`pages/*` +- 通用能力: + - 请求封装:`utils/api.js` + - 登录态:`utils/auth.js` + - 统一错误处理:`utils/errorHandler.js` + - 图片 URL 处理:`utils/imageUrl.js` +- 环境与常量:`config/index.js` + +## 2. 环境与 baseURL(非常关键) + +后端 API 的 baseURL 由 `config/index.js` 管理: + +- `ENV.{development,staging,production}.API_BASE_URL` +- `CURRENT_ENV` +- `REQUEST_TIMEOUT`(默认 30s) +- `PAGE_SIZE`(默认 20) +- `STORAGE_KEYS`(token/user 等 storage key 的唯一来源) + +建议实践: + +- 开发阶段用 `development`,发版/线上联调切 `production`。 +- 不要在页面里硬编码域名或 `/api` 前缀,一律走 `config.API_BASE_URL`。 + +另外,`app.js` 会把 `config.API_BASE_URL` 去掉 `/api` 后写到 `globalData.baseUrl` 并落地到 `storage.baseUrl`,主要给 `utils_new` 那套请求封装使用。正常业务开发推荐统一使用 `utils/api.js` 这一套,避免 token key 和 baseUrl 来源混乱。 + +## 3. 请求封装与 header 格式(统一约定) + +### 3.1 推荐做法:只用 `utils/api.js` + +`utils/api.js` 内部封装了 `request()`,完成以下事情: + +- URL 拼接:`config.API_BASE_URL + url` +- header 默认包含: + - `Content-Type: application/json` + - `Authorization: Bearer `(token 从 `wx.getStorageSync(config.STORAGE_KEYS.TOKEN)` 取) +- 401(未登录/登录过期)处理: + - 非 silent 模式会清理本地登录信息(token/user/userId/expiry) + - 同步 `app.globalData.isLoggedIn = false` + - 尝试调用当前页 `onAuthRequired()`(如果页面实现了该方法) +- `silent` 模式:用于不希望打断用户操作的接口;401 时不清本地登录态,只 reject + +页面层调用建议: + +```js +import api from '../../utils/api' +import { handleApiError } from '../../utils/errorHandler' + +Page({ + async onLoad() { + try { + wx.showLoading({ title: '加载中' }) + const res = await api.user.getProfile() + this.setData({ profile: res.data }) + } catch (err) { + handleApiError(err) + } finally { + wx.hideLoading() + } + } +}) +``` + +### 3.2 不推荐混用:`utils_new/request.js` + +仓库里还有一套 `utils_new/request.js`(更偏调试/可切 baseUrl),但它使用的 token key 是写死的 `auth_token`,而主体系使用 `config.STORAGE_KEYS.TOKEN`(同为 `auth_token` 但请以配置为准)。混用会导致以下问题: + +- 你以为登录了,实际请求没带对 token +- baseUrl 读取路径不同,导致部分页面请求走了另一个域名/端口 + +结论:业务开发优先只用 `utils/api.js`;除非明确是在做本地联调/临时调试,并保证 token/baseUrl 与主体系一致。 + +## 4. 登录态与鉴权(页面开发常见坑) + +核心逻辑在 `utils/auth.js` 和 `app.js`: + +- `app.js` 启动会调用 `checkLoginStatus()`: + - 先本地检查 token + - 再调用 `auth.verifyLogin()` 请求服务端 `/auth/me` 做校验 + - 网络异常时可能允许用本地缓存 userInfo 继续使用(提升弱网体验) +- 页面若必须登录才能访问,建议用 `auth.ensureLogin()` 做统一校验,校验失败会跳登录页 +- token/用户信息写入请走 `auth.saveUserInfo(user, token, expiresAt)` + +页面侧最佳实践: + +- 需要登录的页面,在 `onLoad/onShow` 里先 `await auth.ensureLogin()` 再拉数据 +- 有接口需要“静默拉取”(比如后台刷新余额),用 `api.request(url, { silent: true })` 或 API 方法暴露的 silent 选项(按现有实现为准) +- 需要 401 时做自定义交互的页面,提供 `onAuthRequired()` 方法(例如弹窗提示/引导登录),否则默认行为是清理登录态并由页面自行处理后续 + +## 5. 统一错误处理(减少页面重复代码) + +`utils/errorHandler.js` 提供了: + +- `handleApiError(err)`:统一 toast/提示文案 +- `retryOnError(fn, { maxRetries, retryDelay })`:可重试逻辑(认证错误不会重试) + +建议实践: + +- 页面 catch 里优先调用 `handleApiError(err)`,不要每个页面自己写一套 toast 文案 +- 对用户关键路径(下单/提现提交): + - 网络错误提示要明确 + - 禁止无限重试 + +## 6. 分页与列表加载(约定优先) + +当前工程的分页没有抽象成统一 `paginate()`,更多是“API 方法里给默认分页参数 + 页面侧传参”: + +- 常见参数:`page` + `pageSize`(或部分接口用 `limit`) +- 默认值可参考 `config.PAGE_SIZE` + +建议实践: + +- 页面 data 里维护:`page, pageSize, list, loading, hasMore` +- `onReachBottom` 时判 `hasMore && !loading` 再请求下一页 +- 后端返回是否还有更多,以返回字段为准(如果没有统一字段,就以 `list.length < pageSize` 推断) + +## 7. 上传与图片 URL(最容易漏 token) + +### 7.1 上传 + +使用 `utils/api.js` 的 `uploadFile()`: + +- 上传地址:`${config.API_BASE_URL}/upload` +- 携带 `Authorization: Bearer ` +- 支持 `formData.folder` 指定目录 +- 兼容多种返回格式(`{code:0}` 或 `{success:true}`) + +### 7.2 图片 URL 拼接 + +后端返回相对路径时,用 `utils/imageUrl.js` 的 `getFullImageUrl()` 拼成完整地址(基于 `API_BASE_URL` 去掉 `/api`)。 + +## 8. 头部与页面结构(统一样式框架) + +工程里常见的头部结构是“固定导航 + 状态栏占位 + 标题 + 返回按钮”: + +- 导航容器:`nav-container` +- 状态栏占位:`status-bar` +- 导航栏:`nav-bar` +- 返回按钮:`nav-back` +- 标题:`nav-title` + +常见页面结构示例可参考: + +- 提现页:`pages/withdraw/withdraw.wxml`、`pages/withdraw/withdraw.wxss` +- 充值页:`pages/recharge/recharge.wxml` +- 提现记录:`pages/withdraw-records/withdraw-records.wxml` + +建议实践: + +- 导航容器固定(fixed),内容区通过 `padding-top: {{totalNavHeight}}px` 或 `padding-top: {{totalNavHeight + n}}px` 避免被遮挡 +- 页面根节点常用:``,保证底部安全区 + +## 9. 整体样式风格(如何保持一致) + +### 9.1 全局设计令牌(Design Tokens) + +`app.wxss` 定义了全局 CSS 变量(建议优先使用): + +- `--primary`、`--primary-light` +- `--foreground`、`--muted`、`--border` +- `--radius` + +并补齐了全局基础类: + +- `.btn-reset`:用于 button,去掉默认边框与默认点击态差异 +- `.safe-bottom`:适配底部安全区 + +### 9.2 页面级常用视觉语言 + +在本项目里,“一致感”主要来自以下元素: + +- 背景:浅紫/粉色系渐变或纯色(如 `#E8C3D4`、`linear-gradient(180deg, #F8F5FF 0%, #FFFFFF 100%)`) +- 卡片:白底 + 大圆角(20rpx~48rpx)+ 轻阴影 + 适度边框 +- 主按钮:紫色渐变(`#B06AB3 → #9B4D9E`)+ 胶囊圆角(999rpx)+ 统一阴影 +- 文案层级:标题更粗更深(700~900),说明文字更浅(`#6B7280/#9CA3AF`) + +建议做法: + +- 新页面先找一个“风格相近”的现有页面抄结构与基础样式,再替换业务内容 +- 少做随意的颜色与圆角,优先复用现有渐变、阴影、字号层级 + +## 10. 交互与组件(避免嵌套交互坑) + +约定:交互元素不要互相嵌套(例如可点击容器里再放 button 或另一个可点击 view)。如果需要整块可点: + +- 要么外层用 `bindtap`,内部不用 button(用普通 view/text 模拟按钮) +- 要么使用 button 做唯一点击源,外层不再绑事件 + +这类嵌套会在检查工具/规范中触发警告,也容易造成点击穿透/事件冒泡问题。 + +## 11. 新增/修改 API 的推荐流程 + +1. 在 `utils/api.js` 增加/修改对应模块方法(保持命名与路径一致) +2. 请求一律通过内部 `request()`(确保 header、401、timeout、错误格式一致) +3. 页面侧只调用 `api.xxx.yyy()`,不直接拼 URL、不直接 `wx.request` +4. 异常统一走 `handleApiError`,少在页面写自定义错误文案 + +## 12. 排查清单(定位问题最快) + +- 请求没到后端: + - `config.CURRENT_ENV` 是否正确 + - `config.API_BASE_URL` 是否正确(是否带了 `/api`) + - 是否混用了 `utils_new` 导致 baseUrl/token 读取来源不一致 +- 401/未登录: + - storage 里是否存在 `config.STORAGE_KEYS.TOKEN` + - 页面是否需要 `auth.ensureLogin()` + - 是否误用了 silent 导致 401 没触发清理/引导 +- 样式不一致: + - 是否使用了 `.btn-reset`(button 默认样式会把你“设计稿一致性”破坏掉) + - 是否使用了 `.safe-bottom` + - 是否沿用了已有页面的卡片/按钮视觉语言 +