From eccad4b9d396db6fcbc90ed50e985733f153ae89 Mon Sep 17 00:00:00 2001 From: linshujie Date: Thu, 6 Aug 2026 18:28:48 +0800 Subject: [PATCH] =?UTF-8?q?-=20=E4=B8=AA=E4=BA=BA=E4=B8=AD=E5=BF=83?= =?UTF-8?q?=EF=BC=88UI=E6=94=B9=E7=89=88=E3=80=81=E7=85=A7=E7=89=87?= =?UTF-8?q?=E5=A2=99=E6=95=88=E6=9E=9C=EF=BC=89=20-=20=E6=89=8B=E6=9C=BA?= =?UTF-8?q?=E6=8A=A5=E5=91=8A=E5=8A=9F=E8=83=BD=EF=BC=88=E5=B1=8F=E5=B9=95?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=97=B6=E9=97=B4=E3=80=81=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E6=AC=A1=E6=95=B0=E3=80=81=E8=A7=A3=E9=94=81=E6=AC=A1=E6=95=B0?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 96 +-- QuickLocation.xcodeproj/project.pbxproj | 28 +- QuickLocation/AppDelegate.swift | 1 + .../Common/back.imageset/back@2x.png | Bin 390 -> 2923 bytes .../Common/back.imageset/back@3x.png | Bin 551 -> 5798 bytes .../member_app_usage.imageset/Contents.json | 8 + .../member_phone_report@2x.png | Bin 0 -> 643 bytes .../member_phone_report@3x.png | Bin 0 -> 1221 bytes .../Contents.json | 8 + .../member_daily_unlock@2x.png | Bin 0 -> 801 bytes .../member_daily_unlock@3x.png | Bin 0 -> 1663 bytes .../Contents.json | 8 + .../member_app_usage@2x.png | Bin 0 -> 730 bytes .../member_app_usage@3x.png | Bin 0 -> 1560 bytes .../Contents.json | 8 + .../member_report_clock@2x.png | Bin 0 -> 626 bytes .../member_report_clock@3x.png | Bin 0 -> 1204 bytes .../member_screenTime.imageset/Contents.json | 22 + .../组 48025@2x.png | Bin 0 -> 1460 bytes .../组 48025@3x.png | Bin 0 -> 2156 bytes .../Contents.json | 22 + .../组 48027@2x.png | Bin 0 -> 301 bytes .../组 48027@3x.png | Bin 0 -> 525 bytes .../member_usage_count.imageset/Contents.json | 8 + .../member_usage_count@2x.png | Bin 0 -> 547 bytes .../member_usage_count@3x.png | Bin 0 -> 1046 bytes .../arrow_right.imageset/arrow_right@2x.png | Bin 229 -> 206 bytes .../arrow_right.imageset/arrow_right@3x.png | Bin 229 -> 379 bytes .../Mine/clothespin.imageset/Contents.json | 8 + .../clothespin.imageset/clothespin@2x.png | Bin 0 -> 722 bytes .../clothespin.imageset/clothespin@3x.png | Bin 0 -> 1566 bytes .../clothespin_blue.imageset/Contents.json | 8 + .../clothespin_blue@2x.png | Bin 0 -> 1189 bytes .../clothespin_blue@3x.png | Bin 0 -> 2167 bytes .../clothespin_green.imageset/Contents.json | 8 + .../clothespin_green@2x.png | Bin 0 -> 1243 bytes .../clothespin_green@3x.png | Bin 0 -> 2235 bytes .../clothespin_pink.imageset/Contents.json | 8 + .../clothespin_pink@2x.png | Bin 0 -> 1110 bytes .../clothespin_pink@3x.png | Bin 0 -> 2070 bytes .../customer_service@2x.png | Bin 2792 -> 1908 bytes .../Mine/menu_about.imageset/Contents.json | 8 + .../Mine/menu_about.imageset/icon@2x.png | Bin 0 -> 1768 bytes .../Mine/menu_about.imageset/icon@3x.png | Bin 0 -> 3636 bytes .../Contents.json | 8 + .../quick_data_security.imageset/icon@2x.png | Bin 0 -> 1670 bytes .../quick_data_security.imageset/icon@3x.png | Bin 0 -> 2971 bytes .../quick_permission.imageset/Contents.json | 8 + .../quick_permission.imageset/icon@2x.png | Bin 0 -> 1962 bytes .../quick_permission.imageset/icon@3x.png | Bin 0 -> 3547 bytes .../Mine/quick_privacy.imageset/Contents.json | 8 + .../Mine/quick_privacy.imageset/icon@2x.png | Bin 0 -> 1615 bytes .../Mine/quick_privacy.imageset/icon@3x.png | Bin 0 -> 2689 bytes .../Mine/quick_status.imageset/Contents.json | 8 + .../Mine/quick_status.imageset/icon@2x.png | Bin 0 -> 1564 bytes .../Mine/quick_status.imageset/icon@3x.png | Bin 0 -> 2727 bytes QuickLocation/Common/Constant.swift | 20 + .../Manager/Unlock/UnlockCountManager.swift | 143 +++- QuickLocation/Model/GroupModel.swift | 6 + .../Section/Group/GroupItineraryView.swift | 210 ++--- .../Section/Group/GroupViewController.swift | 5 - .../Group/InviteJoin/InviteJoinView.swift | 2 - .../Section/Home/GroupMemberView2.swift | 52 +- .../Section/Home/HomeViewController.swift | 1 + .../Section/Home/MemberPhoneReportView.swift | 316 +++++++ .../Section/Mine/Account/AccountView.swift | 22 +- .../Section/Mine/MinePhotoWallView.swift | 770 ++++++++++++++++++ QuickLocation/Section/Mine/MineView.swift | 333 ++++---- .../Section/Mine/MineViewController.swift | 82 +- .../Section/Mine/MineViewModel.swift | 79 +- QuickLocation/UIKit/BaseNavigationView.swift | 4 +- 71 files changed, 1825 insertions(+), 501 deletions(-) create mode 100644 QuickLocation/Assets.xcassets/Home/member_app_usage.imageset/Contents.json create mode 100644 QuickLocation/Assets.xcassets/Home/member_app_usage.imageset/member_phone_report@2x.png create mode 100644 QuickLocation/Assets.xcassets/Home/member_app_usage.imageset/member_phone_report@3x.png create mode 100644 QuickLocation/Assets.xcassets/Home/member_daily_unlock.imageset/Contents.json create mode 100644 QuickLocation/Assets.xcassets/Home/member_daily_unlock.imageset/member_daily_unlock@2x.png create mode 100644 QuickLocation/Assets.xcassets/Home/member_daily_unlock.imageset/member_daily_unlock@3x.png create mode 100644 QuickLocation/Assets.xcassets/Home/member_phone_report.imageset/Contents.json create mode 100644 QuickLocation/Assets.xcassets/Home/member_phone_report.imageset/member_app_usage@2x.png create mode 100644 QuickLocation/Assets.xcassets/Home/member_phone_report.imageset/member_app_usage@3x.png create mode 100644 QuickLocation/Assets.xcassets/Home/member_report_clock.imageset/Contents.json create mode 100644 QuickLocation/Assets.xcassets/Home/member_report_clock.imageset/member_report_clock@2x.png create mode 100644 QuickLocation/Assets.xcassets/Home/member_report_clock.imageset/member_report_clock@3x.png create mode 100644 QuickLocation/Assets.xcassets/Home/member_screenTime.imageset/Contents.json create mode 100644 QuickLocation/Assets.xcassets/Home/member_screenTime.imageset/组 48025@2x.png create mode 100644 QuickLocation/Assets.xcassets/Home/member_screenTime.imageset/组 48025@3x.png create mode 100644 QuickLocation/Assets.xcassets/Home/member_screenTime_arrow.imageset/Contents.json create mode 100644 QuickLocation/Assets.xcassets/Home/member_screenTime_arrow.imageset/组 48027@2x.png create mode 100644 QuickLocation/Assets.xcassets/Home/member_screenTime_arrow.imageset/组 48027@3x.png create mode 100644 QuickLocation/Assets.xcassets/Home/member_usage_count.imageset/Contents.json create mode 100644 QuickLocation/Assets.xcassets/Home/member_usage_count.imageset/member_usage_count@2x.png create mode 100644 QuickLocation/Assets.xcassets/Home/member_usage_count.imageset/member_usage_count@3x.png create mode 100644 QuickLocation/Assets.xcassets/Mine/clothespin.imageset/Contents.json create mode 100644 QuickLocation/Assets.xcassets/Mine/clothespin.imageset/clothespin@2x.png create mode 100644 QuickLocation/Assets.xcassets/Mine/clothespin.imageset/clothespin@3x.png create mode 100644 QuickLocation/Assets.xcassets/Mine/clothespin_blue.imageset/Contents.json create mode 100644 QuickLocation/Assets.xcassets/Mine/clothespin_blue.imageset/clothespin_blue@2x.png create mode 100644 QuickLocation/Assets.xcassets/Mine/clothespin_blue.imageset/clothespin_blue@3x.png create mode 100644 QuickLocation/Assets.xcassets/Mine/clothespin_green.imageset/Contents.json create mode 100644 QuickLocation/Assets.xcassets/Mine/clothespin_green.imageset/clothespin_green@2x.png create mode 100644 QuickLocation/Assets.xcassets/Mine/clothespin_green.imageset/clothespin_green@3x.png create mode 100644 QuickLocation/Assets.xcassets/Mine/clothespin_pink.imageset/Contents.json create mode 100644 QuickLocation/Assets.xcassets/Mine/clothespin_pink.imageset/clothespin_pink@2x.png create mode 100644 QuickLocation/Assets.xcassets/Mine/clothespin_pink.imageset/clothespin_pink@3x.png create mode 100644 QuickLocation/Assets.xcassets/Mine/menu_about.imageset/Contents.json create mode 100644 QuickLocation/Assets.xcassets/Mine/menu_about.imageset/icon@2x.png create mode 100644 QuickLocation/Assets.xcassets/Mine/menu_about.imageset/icon@3x.png create mode 100644 QuickLocation/Assets.xcassets/Mine/quick_data_security.imageset/Contents.json create mode 100644 QuickLocation/Assets.xcassets/Mine/quick_data_security.imageset/icon@2x.png create mode 100644 QuickLocation/Assets.xcassets/Mine/quick_data_security.imageset/icon@3x.png create mode 100644 QuickLocation/Assets.xcassets/Mine/quick_permission.imageset/Contents.json create mode 100644 QuickLocation/Assets.xcassets/Mine/quick_permission.imageset/icon@2x.png create mode 100644 QuickLocation/Assets.xcassets/Mine/quick_permission.imageset/icon@3x.png create mode 100644 QuickLocation/Assets.xcassets/Mine/quick_privacy.imageset/Contents.json create mode 100644 QuickLocation/Assets.xcassets/Mine/quick_privacy.imageset/icon@2x.png create mode 100644 QuickLocation/Assets.xcassets/Mine/quick_privacy.imageset/icon@3x.png create mode 100644 QuickLocation/Assets.xcassets/Mine/quick_status.imageset/Contents.json create mode 100644 QuickLocation/Assets.xcassets/Mine/quick_status.imageset/icon@2x.png create mode 100644 QuickLocation/Assets.xcassets/Mine/quick_status.imageset/icon@3x.png create mode 100644 QuickLocation/Section/Home/MemberPhoneReportView.swift create mode 100644 QuickLocation/Section/Mine/MinePhotoWallView.swift diff --git a/.gitignore b/.gitignore index 6b7678fd..73d65739 100644 --- a/.gitignore +++ b/.gitignore @@ -45,96 +45,6 @@ npm-debug.log *.png.orig *.jpg.orig screenshot/ -.opencode/skills/README.md -.opencode/skills/ask-matt/SKILL.md -.opencode/skills/ask-matt/agents/openai.yaml -.opencode/skills/code-review/SKILL.md -.opencode/skills/code-review/agents/openai.yaml -.opencode/skills/codebase-design/DEEPENING.md -.opencode/skills/codebase-design/DESIGN-IT-TWICE.md -.opencode/skills/codebase-design/SKILL.md -.opencode/skills/codebase-design/agents/openai.yaml -.opencode/skills/diagnosing-bugs/SKILL.md -.opencode/skills/diagnosing-bugs/agents/openai.yaml -.opencode/skills/diagnosing-bugs/scripts/hitl-loop.template.sh -.opencode/skills/domain-modeling/ADR-FORMAT.md -.opencode/skills/domain-modeling/CONTEXT-FORMAT.md -.opencode/skills/domain-modeling/SKILL.md -.opencode/skills/domain-modeling/agents/openai.yaml -.opencode/skills/grill-me/SKILL.md -.opencode/skills/grill-me/agents/openai.yaml -.opencode/skills/grill-with-docs/SKILL.md -.opencode/skills/grill-with-docs/agents/openai.yaml -.opencode/skills/grilling/SKILL.md -.opencode/skills/grilling/agents/openai.yaml -.opencode/skills/handoff/SKILL.md -.opencode/skills/handoff/agents/openai.yaml -.opencode/skills/implement/SKILL.md -.opencode/skills/implement/agents/openai.yaml -.opencode/skills/improve-codebase-architecture/HTML-REPORT.md -.opencode/skills/improve-codebase-architecture/SKILL.md -.opencode/skills/improve-codebase-architecture/agents/openai.yaml -.opencode/skills/prototype/LOGIC.md -.opencode/skills/prototype/SKILL.md -.opencode/skills/prototype/UI.md -.opencode/skills/prototype/agents/openai.yaml -.opencode/skills/research/SKILL.md -.opencode/skills/research/agents/openai.yaml -.opencode/skills/resolving-merge-conflicts/SKILL.md -.opencode/skills/resolving-merge-conflicts/agents/openai.yaml -.opencode/skills/setup-matt-pocock-skills/domain.md -.opencode/skills/setup-matt-pocock-skills/issue-tracker-github.md -.opencode/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md -.opencode/skills/setup-matt-pocock-skills/issue-tracker-local.md -.opencode/skills/setup-matt-pocock-skills/SKILL.md -.opencode/skills/setup-matt-pocock-skills/triage-labels.md -.opencode/skills/setup-matt-pocock-skills/agents/openai.yaml -.opencode/skills/tdd/mocking.md -.opencode/skills/tdd/SKILL.md -.opencode/skills/tdd/tests.md -.opencode/skills/tdd/agents/openai.yaml -.opencode/skills/teach/GLOSSARY-FORMAT.md -.opencode/skills/teach/LEARNING-RECORD-FORMAT.md -.opencode/skills/teach/MISSION-FORMAT.md -.opencode/skills/teach/RESOURCES-FORMAT.md -.opencode/skills/teach/SKILL.md -.opencode/skills/teach/agents/openai.yaml -.opencode/skills/to-spec/SKILL.md -.opencode/skills/to-spec/agents/openai.yaml -.opencode/skills/to-tickets/SKILL.md -.opencode/skills/to-tickets/agents/openai.yaml -.opencode/skills/triage/AGENT-BRIEF.md -.opencode/skills/triage/OUT-OF-SCOPE.md -.opencode/skills/triage/SKILL.md -.opencode/skills/triage/agents/openai.yaml -.opencode/skills/wayfinder/SKILL.md -.opencode/skills/wayfinder/agents/openai.yaml -.opencode/skills/writing-great-skills/GLOSSARY.md -.opencode/skills/writing-great-skills/SKILL.md -.opencode/skills/writing-great-skills/agents/openai.yaml -.cursor/commands/opsx-apply.md -.cursor/commands/opsx-archive.md -.cursor/commands/opsx-explore.md -.cursor/commands/opsx-propose.md -.cursor/commands/opsx-sync.md -.cursor/commands/opsx-update.md -.cursor/skills/openspec-apply-change/SKILL.md -.cursor/skills/openspec-archive-change/SKILL.md -.cursor/skills/openspec-explore/SKILL.md -.cursor/skills/openspec-propose/SKILL.md -.cursor/skills/openspec-sync-specs/SKILL.md -.cursor/skills/openspec-update-change/SKILL.md -.opencode/commands/opsx-apply.md -.opencode/commands/opsx-archive.md -.opencode/commands/opsx-explore.md -.opencode/commands/opsx-propose.md -.opencode/commands/opsx-sync.md -.opencode/commands/opsx-update.md -.opencode/skills/openspec-apply-change/SKILL.md -.opencode/skills/openspec-archive-change/SKILL.md -.opencode/skills/openspec-explore/SKILL.md -.opencode/skills/openspec-propose/SKILL.md -.opencode/skills/openspec-sync-specs/SKILL.md -.opencode/skills/openspec-update-change/SKILL.md -openspec/config.yaml -openspec/CONVENTIONS.md +.opencode/ +.cursor/ +openspec/ diff --git a/QuickLocation.xcodeproj/project.pbxproj b/QuickLocation.xcodeproj/project.pbxproj index 09eb7392..50c0c10a 100644 --- a/QuickLocation.xcodeproj/project.pbxproj +++ b/QuickLocation.xcodeproj/project.pbxproj @@ -96,7 +96,6 @@ 305A76DE2FCA8C7000227D26 /* ObservableType+ObjectMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76202FCA8C7000227D26 /* ObservableType+ObjectMapper.swift */; }; 305A76DF2FCA8C7000227D26 /* Single+ObjectMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76212FCA8C7000227D26 /* Single+ObjectMapper.swift */; }; 305A76E02FCA8C7000227D26 /* GroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76232FCA8C7000227D26 /* GroupView.swift */; }; - 55B217B13022F0A000784711 /* GroupItineraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217B03022F0A000784710 /* GroupItineraryView.swift */; }; 305A76E12FCA8C7000227D26 /* GroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76242FCA8C7000227D26 /* GroupViewController.swift */; }; 305A76E22FCA8C7000227D26 /* GroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76252FCA8C7000227D26 /* GroupViewModel.swift */; }; 305A76E32FCA8C7000227D26 /* GroupMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76272FCA8C7000227D26 /* GroupMemberView.swift */; }; @@ -298,6 +297,11 @@ 55B2179330218CE100784774 /* GroupMemberView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2179230218CE100784774 /* GroupMemberView2.swift */; }; 55B217953022DD6B00784774 /* 荆南波波黑-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 55B217943022DD6B00784774 /* 荆南波波黑-Bold.ttf */; }; 55B217A13022F0A000784702 /* FontManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217A03022F0A000784701 /* FontManager.swift */; }; + 55B217B13022F0A000784711 /* GroupItineraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217B03022F0A000784710 /* GroupItineraryView.swift */; }; + 55B217C13022F0A000784721 /* MinePhotoWallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C03022F0A000784720 /* MinePhotoWallView.swift */; }; + 55B217C33022F0A000784723 /* MemberPhoneReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C23022F0A000784722 /* MemberPhoneReportView.swift */; }; + 55B217C53022F0A000784725 /* MemberAppUsageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C43022F0A000784724 /* MemberAppUsageView.swift */; }; + 55B217D13022F0A000784727 /* UnlockCountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D03022F0A000784726 /* UnlockCountManager.swift */; }; 55B21D6E3023117900784774 /* 优设标题黑_猫啃网.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */; }; 55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; }; 55BF75352FFF91690055DA57 /* InteractionEmojiCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */; }; @@ -399,7 +403,6 @@ 305A76202FCA8C7000227D26 /* ObservableType+ObjectMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableType+ObjectMapper.swift"; sourceTree = ""; }; 305A76212FCA8C7000227D26 /* Single+ObjectMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Single+ObjectMapper.swift"; sourceTree = ""; }; 305A76232FCA8C7000227D26 /* GroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupView.swift; sourceTree = ""; }; - 55B217B03022F0A000784710 /* GroupItineraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupItineraryView.swift; sourceTree = ""; }; 305A76242FCA8C7000227D26 /* GroupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupViewController.swift; sourceTree = ""; }; 305A76252FCA8C7000227D26 /* GroupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupViewModel.swift; sourceTree = ""; }; 305A76272FCA8C7000227D26 /* GroupMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberView.swift; sourceTree = ""; }; @@ -610,6 +613,11 @@ 55B2179230218CE100784774 /* GroupMemberView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberView2.swift; sourceTree = ""; }; 55B217943022DD6B00784774 /* 荆南波波黑-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "荆南波波黑-Bold.ttf"; sourceTree = ""; }; 55B217A03022F0A000784701 /* FontManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontManager.swift; sourceTree = ""; }; + 55B217B03022F0A000784710 /* GroupItineraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupItineraryView.swift; sourceTree = ""; }; + 55B217C03022F0A000784720 /* MinePhotoWallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinePhotoWallView.swift; sourceTree = ""; }; + 55B217C23022F0A000784722 /* MemberPhoneReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberPhoneReportView.swift; sourceTree = ""; }; + 55B217C43022F0A000784724 /* MemberAppUsageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberAppUsageView.swift; sourceTree = ""; }; + 55B217D03022F0A000784726 /* UnlockCountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockCountManager.swift; sourceTree = ""; }; 55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "优设标题黑_猫啃网.ttf"; sourceTree = ""; }; 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = ""; }; 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionEmojiCell.swift; sourceTree = ""; }; @@ -928,6 +936,7 @@ 305A75262FCA8C7000227D26 /* Money */, 305A75282FCA8C7000227D26 /* Notification */, 305A752A2FCA8C7000227D26 /* Theme */, + 55B217D23022F0A000784728 /* Unlock */, 305A752C2FCA8C7000227D26 /* URL */, 30D87CD52FDF9F1900E958FD /* MQTT */, 30C6666A2FFB7C3000E62B25 /* IAPManager */, @@ -1021,6 +1030,8 @@ 305A76282FCA8C7000227D26 /* HomeView.swift */, 55B2179030217D6600784774 /* HomeView2.swift */, 55B2179230218CE100784774 /* GroupMemberView2.swift */, + 55B217C23022F0A000784722 /* MemberPhoneReportView.swift */, + 55B217C43022F0A000784724 /* MemberAppUsageView.swift */, 30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */, 30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */, 30D87CDC2FDFF07500E958FD /* InteractionView.swift */, @@ -1071,6 +1082,7 @@ 305A76372FCA8C7000227D26 /* MineViewController.swift */, 305A76382FCA8C7000227D26 /* MineViewModel.swift */, 305A76362FCA8C7000227D26 /* MineView.swift */, + 55B217C03022F0A000784720 /* MinePhotoWallView.swift */, 30EFF3CB2FDA667A00EB35D4 /* MyProfile */, 30EFF3BC2FD9585200EB35D4 /* Account */, 30EFF3D42FDA8EF200EB35D4 /* EmergencyContact */, @@ -1683,6 +1695,14 @@ path = Font; sourceTree = ""; }; + 55B217D23022F0A000784728 /* Unlock */ = { + isa = PBXGroup; + children = ( + 55B217D03022F0A000784726 /* UnlockCountManager.swift */, + ); + path = Unlock; + sourceTree = ""; + }; B07D45692FCECE07570D9B73 /* Pods */ = { isa = PBXGroup; children = ( @@ -1961,6 +1981,8 @@ 30BF300C2FED09BA00D9CB52 /* ScheduleDetailView.swift in Sources */, 305A76C22FCA8C7000227D26 /* BaseViewModel.swift in Sources */, 55B2179330218CE100784774 /* GroupMemberView2.swift in Sources */, + 55B217C53022F0A000784725 /* MemberAppUsageView.swift in Sources */, + 55B217C33022F0A000784723 /* MemberPhoneReportView.swift in Sources */, 30A87A622FEE724D0095E7C6 /* CreateBubbleTiemView.swift in Sources */, 30DC185A2FD11E7A0041DCD1 /* WebOperations.swift in Sources */, 30A87A642FEE75520095E7C6 /* CreateBubbleTipsView.swift in Sources */, @@ -1992,6 +2014,7 @@ 305A76D02FCA8C7000227D26 /* MoneyFormatter.swift in Sources */, 305A76D12FCA8C7000227D26 /* TimeSpecificNotificationManager.swift in Sources */, 305A76D22FCA8C7000227D26 /* ThemeManager.swift in Sources */, + 55B217D13022F0A000784727 /* UnlockCountManager.swift in Sources */, 305A76D32FCA8C7000227D26 /* URLManager.swift in Sources */, 305A76D42FCA8C7000227D26 /* GroupModel.swift in Sources */, 305A798C2FCAB99300227D26 /* HomeViewModel.swift in Sources */, @@ -2055,6 +2078,7 @@ 305A76ED2FCA8C7000227D26 /* MemberAnnotationView.swift in Sources */, 30BAB8532FCD337C00C33B5C /* GroupService.swift in Sources */, 305A76EE2FCA8C7000227D26 /* MineView.swift in Sources */, + 55B217C13022F0A000784721 /* MinePhotoWallView.swift in Sources */, 305A76EF2FCA8C7000227D26 /* MineViewController.swift in Sources */, 30A87A5E2FEE71A50095E7C6 /* CreateBubbleView.swift in Sources */, 30D74ABF2FEA67F30050EB2C /* CreateScheduleView.swift in Sources */, diff --git a/QuickLocation/AppDelegate.swift b/QuickLocation/AppDelegate.swift index c57ea5dd..698f9128 100644 --- a/QuickLocation/AppDelegate.swift +++ b/QuickLocation/AppDelegate.swift @@ -21,6 +21,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + UnlockCountManager.shared.start(launchOptions: launchOptions) FontManager.registerCustomFonts() setupLocation() ApiManager.shared.setup() diff --git a/QuickLocation/Assets.xcassets/Common/back.imageset/back@2x.png b/QuickLocation/Assets.xcassets/Common/back.imageset/back@2x.png index 2e36c2ca8dd21b365ab2623b9c92d25daf322a06..856813817618a7245f4a887a021b974fd4894018 100644 GIT binary patch literal 2923 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 diff --git a/QuickLocation/Assets.xcassets/Common/back.imageset/back@3x.png b/QuickLocation/Assets.xcassets/Common/back.imageset/back@3x.png index 854c16469f92b232d4aa7fc3f7ea956a6f97c198..554426141b27d291969fb7161204f50b58dcc4d7 100644 GIT binary patch literal 5798 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 literal 551 zcmV+?0@(eDP)1}}+}-!{T)@=f6f6A$jOeVRV8PDSK@dA;uyHOZB2F&i zkI6-yL^oRy>=dO-8;EGzD{aZc( zM&k4MjC_BkP2qDje8lcykUkYQy^LRj9OPIOrd9!0(e*L^^j&>YnA|n2`kcOYYE#0L z+`tBMzm{i;!jp~a^&qF`9tan1V?E5FUOVM6oyR8pu*f->a1Zg<=WM16!ZclUOW5)` zf82aF&dpiD|3QxS;#q1APND5{cCnsZ5hk2L8+|{lHJy_JH|KRQtZvkx=A>u!1$G9* z(jQSnm{4)=k^A)>7iC0v;#9u|IiFCW=0sKf;$yb)wpk(%E6B;+D^!FF?I7p*h10@@ zN67tpR^RS37@+eB;|NAQxrC|g2DB%T$`c~PUnUn>G2Oe@7b~N@PqRt pIx)W%^s#p0ru}3w#u#JFQF|6mW56w)#fAU?002ovPDHLkV1nwDKkYVGesyfL?|>wDKteWG({;i zL?|;vDKkVUG(;&hMJY5zEH_9sJ5M`7R6IsjKuTFdOoRpcOnw_VepsApvtgy1Wv$egpxWBo(!oR`D%+1r! z(bd`7-rU{b;^XG!=IQ6?>gnn0>+J39?C$UH@$&NY_4W4n`1$|;|0{?`GXMYp7j#li zQve7I5FjKcC|8S@o134<%g^KI>+kpbw>AIP0003}NklZv~7zJS2caT7W zxYf4WR=d}(R%^-oKU5(JkN`T<3(pnFd=r?=f9Um264`VEfMDc1rgDH7HcYC-p4LGm ziz7vlB{hxOPg?1#qS3>oBS06<0ZC^)Sxwm+>+iDY&LYG3XPsw zG={Dk-|4Cx-{q_|zCAVUE&>orrFYgP)c;@3qUJ~>uwC<_G+xXR+c@xFQB22=AOeAF d%BT6c{Q=^mZHJC96z~85002ovPDHLkV1h*$DO&&l literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Home/member_app_usage.imageset/member_phone_report@3x.png b/QuickLocation/Assets.xcassets/Home/member_app_usage.imageset/member_phone_report@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..8f20f18de585efad91899bf9082669741538467a GIT binary patch literal 1221 zcmeAS@N?(olHy`uVBq!ia0vp^W+2SL3?z5Yp7oxAfpJNIPlzj!3kD4i4i3h~#tIG& z1_lngHr^U$&T7UE%BF7WW^QW6Zd&^8TE=dgMlM=L?rMgv8b)p!Ca#($?ixlenx;Uu zizbk3?5bt#s%{KqxM&zd6u4;ub(y+>WRVe2jVVL~Bn8w3B0&nk<{+ztv31Nn^ejCM zt$hq_e2hTI*Tl}>)Y{*|F38d@$jTwa$|c0sIn3TA+{ruI**DtFC(b7{*(Wr`H!Q_3 zB-KAOEif!SEGj!St1>;is;I1`xT>|JqOH8Tv$D3Us;;}PslT;zYG?0^$`ybt2eJ+yLIdK{k!)b-o5|m-n~cn?mv3)@X13Udi3<^vzISkzJB@g z&C6GBU%h_&`pvsHZ{B_S{N?kPuU~-Z>$mUUfBgFO>-X2=0t|_q*+>8%v_)wQ6ne z%kN(3t@qMBeP+|)Hz7hT^}CMemVKL*_|X0S{DgZyZO$;CuDRdX_&HKbH}U)NM?bZu z`n52dIho&JzBFUcL=oK;A}`HWPn}{oV@ZbRRHKCt*Bv*maLY^)t*sKB7P&Z8Dp(}* zpr-per{vYsN);|n*z8cg*6_MO(@wXHR;fZIKMObQFvlvtsjA$wCv$gSXW43E&lqjF z`p54plTFu|Zm+xWisMyDeQMw9&_!&5pAX)zC|h2+?dM;k=-uh-XW!l&v;B?Ws)tH8 z?teS}_E=B9x$Epc%XNppt(X}8N+3oqK6kUV{hqY!_x~NQAA8ugfX%3wZJl3kiNID% zUUL=SIZ`FlAG|sv)|Gqqga6Fwzl};&?piNajylj0Vz1-Mkhb`D;uhHr4)tgFBP0&j z%~gox*}yVst6WS?SyNKsflD6W{<0PS=Q#SQA<*br#n+Xe-&z&F=kE$-Tz%?>_p>bF z(;5dp8GUO>tV@WL+E5|>t=RvVP{Yv=Z$5~9b~|;b;pfLUpIOr)Pcb#z{P;#$Io<5d z;yb+u-6N$ZZd!h3>9O~-&m2BBUHeQir*i3?+~-SG-n?GCeeUJ*^77mD@iulMn3^15Dem5G%6{I^aKB1V-_cgj8>ga{1isPG zV81G%6fpPH#urYl8@v@IxX&q;JW0>42svq4u(+vpNx>?at&fgN?p@yzwpgxt)ole6 znNw3z7W)`wgh=hWl4h7`N#M8$oBZi_xQ*6`N#M9$M^Xv%hfE) z)i2D}G0oOA&DS>0*ErAEJI~oY(AhxI+C|daN7LI%)Z9$e+)vfrQP$m2*4|Ur-c;7! zR@dKH*xy;$-&@$=V%p(i+Tv&2<7(XFY~AE`;O2SY=6T`gd*SDO;^=_m=!4_wh2-go zWt>=kLT=>=j@c|?Uw27nd$DC>h7EB?wjiGp6l+hiJ@T2VTrS0*k?(wSb z@~rRjukiD+@bj_o^tAExw(|A4^7X#-_Q3V`!}a&W_4vj1_{jJ9X8-630000HbW%=J z03RS%TXT1NpQpFG^BgmDsaE;@nH zWK2A&V=Oh#`~9Gfr6Vd&N>pdq;T=_33RFLRY^naHDoTd23v9VyEG>@9*x;}VOZCM! z#=L|`o_C^Q|627&Q{Ksmh~sjviX(GL?v)ZF%RRE9QWZYHP0EUs^LP(8?}#mtE|xuR f@JfY@BTN4Qdf;wI>p$~c00000NkvXXu0mjfy9czW literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Home/member_daily_unlock.imageset/member_daily_unlock@3x.png b/QuickLocation/Assets.xcassets/Home/member_daily_unlock.imageset/member_daily_unlock@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..ddf41c2377246d3326fc975be2cb0803794d5fb3 GIT binary patch literal 1663 zcmW;Ld010d769-EQH!zKpo0o%(W(^$N-0Ds(2+$Nf^37Jf)JLv;7nN*h#)v3V3pXo zAcjQ&K@kK&+;9Q3$Sz9?l8r17LN-DYlDs63_4UQ?``vrbIp6v3oSPpUh+B)cKm!1< z7Vmo?1bN*J#{^}ld}reU0LCG~{-Hid3d3+64EMq?6^1)txC@3qYPH=uT{i^vL2w5M ze$vi&gSu{Tu1`1D1L}LUP!9z4>Y!fzd>=I5tDoyZ>UyZx5F657-4{q;h@l?+9MaYY z&3$S3Qq<4w>8IKD)9erfGAWxo?+9c3D04i5IdKfZnuucY2`qjziyt$_k7G~% z#-2RE7Mx^H#&ZOx5nRFlxKqD#rxFmnsU)5-nI}BM6P_Ixri_bH$AziLJt0b)5S=p+ zr}M=beDQg{ICD~*H7U6$kYpQ3FA1bM0_o)`#Pnt1^i|<>zHpi(l#xWTYa-d7A{kjE zyDpx&iIB|PlFSrH>`+P4~n?wv;0f&g`` z8eJ=>YctTl2lXF7{a*;JzFn*TsD(OoP^S*+(m~yZ9WV?Re{e0bU4V6JNZ=vB2(=Dl zy3yQn=l2fS@bK`McyeLM-I~T%Egd}r)R8Hf288ApmX;^W*zo{>dWk>a8S2|pR&*+Q zgD2)n5n+Xw(%1BO@cL0F=1Mo+^|ozsE7y>!j#|el%`#VFx8>(x?-R~y+Omw!v3?=m zz6T$9%gRuF>9lhu9e8~Vq$UQc=+kKJ+aW;Qc&c~abxc8p@r4$WjVCNngX#`t7(#f6qIV*t2G zNlo60zMbp4LLd09-2W?Uo4flzL>gy{#yNK_@c!;C3+;B+go|@ry;amXBIU2njf$aw zo%yGl9gI@SE=cs$y{kV(&$h0|yHYu;nle1xz+AvG{R#zt*5xqHys%B>zRqq%dI#H? z-?f~UEdVvx>5bs>%_u<;o=>W5cAs#35?Qu_u#20#hQI7LTZxU% zo3(x6z+Chj=TO~+gbEDt@Z&&iu1h-IcpDb>?DUksslVq1DUE2S z|Km`VHT=_(D*8YtAjxTbW(ev^?#MQU7Z;g2oozzr2dGY2AH2RsOindwH+}RF+@f+RE-Q&AeT@b& aFc8+&{FL;YYaH_D0lZJ(fkv;GoPPlM*Ukd~ literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Home/member_phone_report.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/member_phone_report.imageset/Contents.json new file mode 100644 index 00000000..712671e0 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/member_phone_report.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "member_app_usage@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "member_app_usage@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/Home/member_phone_report.imageset/member_app_usage@2x.png b/QuickLocation/Assets.xcassets/Home/member_phone_report.imageset/member_app_usage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..542712a1ce6c995f91b76ef0781170b06005f29c GIT binary patch literal 730 zcmV<00ww*4P)wDKkYVGesyfL?|>wDKteWG({;i zL?|;vDKkVUG(;&hMJY5zEH_9mI!ZG=O*TMKIYd)EM^->eSwl@+M^Il;T4z*UYFA)w zTVrruW^-X_c42CEWo&t6ZFy*Ld~0)mZ+3!pe1~~}ihP5OhK!bpkeQ8>oRgWNl$oNJ zoTZ(ks-B{%p{A{*sj#cBwz9UowYk5zyurAmaa$<4{k(9F-%(9+i3-{R!u z=jZ6^>g(<9@9^>R^Y!)i`1twz`~3g^|LXPE!2kdN7j#liQve7I5FjKcC|8S@o134< z%g^KI>+kpbw>AIP0004nNkl%aXNEJLLm9;IrKPcUUv{iX4LO^b*k2C8|22l29-I!!H&pxXpbA9Byed*bBGFI zlM37rm4+_moB%~}vOp?uDC48($(r9I<5Q{;L7hH_b-{dwjA_wE4`TYA`6JXVKTMEo zlT{x>+_G3eeBu~erbfzxI9Rh-%>YbaPEp{I>OeeYD@Nwv+3Q27E~!4mFZXi*FNH$9G?@&gZZqQW4Y%iytU6q(WFRi))mONDW}k z?^(RXzCNigHeRtNk{687DyQaG9`5P9Xw^a_$gYv&*0V{(e1y2wf^@ zhNv)X(hfI3N#N2R$B(eX_zsmddSm2FyG7ONUZ+1YQ8?&&HF^~P0g>ftGD++4g#Z8m M07*qoM6N<$f|^`Qw*UYD literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Home/member_phone_report.imageset/member_app_usage@3x.png b/QuickLocation/Assets.xcassets/Home/member_phone_report.imageset/member_app_usage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..16400a4c3f3eb047165bce1711ecf98c78f2d31e GIT binary patch literal 1560 zcmYk6c{tQt7{{k%xyoMM($%IgQ^JfbcQDO9#xi&8Wf)^A8pHB=nd){;2?|c3_&znH9#K}l2OM^fl83GS5LOv1%|ZL<-mK2N?1#9I@AiJ8` z(1Q1dGa|g8_GpnXS$s;-QESj$B}mFoSWY{_sbN|0IFAzt9P)6PpGGF zn3sP9J%Hg880iyq-8YzdDTL`48XXW86A%^~91$NHnG_b86cLqtEtyVaXUXVr{LzD2ehtuM>6z)7+3A_N*}3_-`GtkWrNyP?rKRQNWuZ{`_3KxWNVFna0a#rD zSY2KH_ET#=v<_%vW8?ex@0**OSCkrPz*jQ}BwJd8wr$@bE+wy^v|Htf#t8#MW0bk0 zvxhf5IOKYKMs{%tms{D%8|IIXPfUKA`ZBk;{Po+~`o?!hFWxi=B&J3{pRw7uzqd~0 zrWCYKa#ZwmAh7Uiq3X~VBa?gwd1A^#a|{L%uOu$1Aq_!^*BO%r*_E==eDuqsz zJ&3{AoE8O#oRx*?JAVk3&4tI5k6_R90*!Wc3thNRo`@AlD0XN^wD7BLmpX~I!8X^r z7u?o3^*sb++|*xtS#qkL(?WwBp8r}0G0t`WVI5GnW;1VIm;aU!Sg#xY35v6bQ^3Iz zBpmEm+%fdNVP!&(s^{8q{qQo09B;f0wX+yTzjDdA#wJap;g(>Idjwj2&|dTEEWvP3 z%DlLadN{MOVIpIm`y(?rp$f)riou)e;?9a*w1be1}!fLG7Wm?F`0XbWA3 zHvM|?tqhJ7R2@q&KW#zsF>aN$4te!1jmYVN^beZW$&S>KZ=37zR7$It*ir+@$YuA` zymC4H+>kD=w=_~q-PV(0O5Z#42kUZs3`KjF1QeeysCwmFm$-4hKTyzdfO?^$yQOnP zywC$zyDF?7VQgIcmM_0+=$b#+JL+|wWU%oIMzdLbCC#4*p2a#*fC+er@#QdFShd!Q zaz@0()jkbh%=1VWQJq!tvy>ZCZHM0Nlzd(Uxv7Zn)ckEnu2dgKaGy*gOw}^mvYR|3 j?s9W$^WNMjcv~^WkY!j|D(^)4Z_RfC#uEJ)NoD>A#hmS5 literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Home/member_report_clock.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/member_report_clock.imageset/Contents.json new file mode 100644 index 00000000..fd3dcff5 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/member_report_clock.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "member_report_clock@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "member_report_clock@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/Home/member_report_clock.imageset/member_report_clock@2x.png b/QuickLocation/Assets.xcassets/Home/member_report_clock.imageset/member_report_clock@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..7c9de47a17833afb8659bc3a791c6c62983c4e3b GIT binary patch literal 626 zcmV-&0*(ENP)4^t%7_y8rdN|Ma^5^Sb}^x&QRJ|Ma>4^Sb}^y8raL|H?Q2$~phb zJO9i-|II=F&O`ssMgPx7|IbPP&`bZ)QUBCe|J7Ol)m#7BX8+k}|JrK*-E#lkbpPIV z|KE83-+KSxeE;Bn|K^VW=8^yCm;dRR|LU9n>Ye}Vpa1Nm|L&*%?y3LptN-w=|M0K> z^SJ-?y0}JH#sB~S5Oh*bQve?zS6g#;d!MJbyW{is`|Y|7B>(^cY)M2xR5;6(m0Meb zFbsx6fsK`fK$h0ik9X7|1>YyO50JH-xY1%C!x)u0E5^fh+@(dN1@506cB8Z<}V%wM`$uQZA*db3{bMb=7Gu zU1`aS6orzqmrM%?$Hgu~McHn&1rUkvZHk)GtuRI)esOokA*RA-4C1gEPAC({7hoxj z5BzZ?3mxndVcXC~3f^g8h0`F`8n`xrxe9VLf!HfxYX(~dY$VGIy~$u>1fS^p#t6!x zlTYH$rqI0{?Dm6be}7tW^?Gt8bHxuGUXO*T-D9oh`1<|uHGEZi3iaB#8xEA{DWn1? zRi0H3pDr`e4wo11#jOxr)?6Vhz#2NdN!< literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Home/member_report_clock.imageset/member_report_clock@3x.png b/QuickLocation/Assets.xcassets/Home/member_report_clock.imageset/member_report_clock@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..7bca7e2ddadfcc27c332d9c3dc1f534722e8d78b GIT binary patch literal 1204 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 A-~a#s literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Home/member_screenTime_arrow.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/member_screenTime_arrow.imageset/Contents.json new file mode 100644 index 00000000..d87a79a4 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/member_screenTime_arrow.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "组 48027@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "组 48027@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/Home/member_screenTime_arrow.imageset/组 48027@2x.png b/QuickLocation/Assets.xcassets/Home/member_screenTime_arrow.imageset/组 48027@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..64648288ef326188425a52bd19cb4daf382b14ce GIT binary patch literal 301 zcmeAS@N?(olHy`uVBq!ia0vp^5Ie)|07>yMv*{=S|;y%4A}(bL5-L_#w6Ag^?z zfkfNI-$z@V?6lwf{BJKDYkd2aOxdiht*g^w7@nIxyk4_qmc^%3e^EJZx2;^Jj?2&V z&i9(hdoi+8xZZpHi>*o*9Jg*rpq0!1g~N+~|iK)LW`KVm+9GAvxfNW@$t1IWy?Eiyu^x*+=mHQ$N8iVWGinx$#m zjx$V={1l2*VhG217^YVS;?fZ{Qs4y(loa1Z~Lw)p@#b#{j4 zavIVo61)<7LFNd5l78QM$)yGDOid$2{eF1Q1|HNh$M(LvqK*j>=IbATRJWk?+bN%a?{jrJ<|Cp7J# zwBEF%+U=O&^U&*>4B0Gu#13cyoku4`En1=W7}u@P3ENPUz5xIL|NjF3w-D-X$}A~^ P00000NkvXXu0mjfDzWTf literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Home/member_usage_count.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/member_usage_count.imageset/Contents.json new file mode 100644 index 00000000..35d7e6a8 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/member_usage_count.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "member_usage_count@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "member_usage_count@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/Home/member_usage_count.imageset/member_usage_count@2x.png b/QuickLocation/Assets.xcassets/Home/member_usage_count.imageset/member_usage_count@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc01a8bc04cfef660a280b13e984207cc093d204 GIT binary patch literal 547 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*Fy#2?0JKu0Z4`uYdG^!-M}D zf#@NKu>nkixIi`xfwaV%ie$T)5 zd%=z0i|+hgb@$&Ip!r^loxOo>5-17s3ucg42#+o6beh z-0P>j&Q@feswp%>J+MrrGn4H$66U3Qm-}bI5Fmwe60a)>CZLZgCmTzM8bo-7Kdm zVP0%4$J~_ZpZX7Wy;^nnJmVtEi9$?iC!+;584k>4+xwqY^TFMo6H{+<{x;a0H1Wk; i?!NgwvhsxsHp+)v?01`dxKs-mvJ9TCelF{r5}E+aV;Y$N literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Home/member_usage_count.imageset/member_usage_count@3x.png b/QuickLocation/Assets.xcassets/Home/member_usage_count.imageset/member_usage_count@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..faa3697965ddabf08a4c1662c74d24aad18712cf GIT binary patch literal 1046 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw3=&b&bYNg$^a}6^aRqY!|Np=0|EjnDx8480 z{^9>EFaEE4@PF;i|EnJV-}vPJ)_ecgJ^H^1D0Lr7ZhY_`h&DX@zwzP!O<*>h3zY&2 z-UrDd6ad*Ul_14H_Vh=Z(;sQg03q!ek9B4~)}8fOZ`Nb|*^dooKQWl|#AxmllX*|g z<~=o^4@A!_7d*9E^4xj(3%BJj-B-NyT=~*_4yAyb0g@ zHg3z?_^s~}x4ui-_AY7r`_vup({{X1-}yds|EJP}pDPZ1t~~Ok_W0MP<6oOkerr4V zt^M@3uCqV-F8r8!;pgOwKc`;$IsMx2`8WP7zWHa#tv^d|{aJSF@3Px}m*4)o;`ZN_ zcmA%r`)~E#e{1gjTYK-{y8Hit!2(1eAD!6v_5(09HA;f~f*BZ@`2+;TB&Ai<^i3lp z6Eb?I%$&V^-PYZQkDobz>B*DVpTGS0_51HXpHmB37#Ns>JzX3_JdVGeezW*efXFfa z-$6p(<_cYFni8Qa*u6rdBO>Zx)Zz{jJXP0$l-|J~xT>~~Bx+ePOY3R;CDHCzuouSy;Zs1U)^?_$6S~z*)MoTu`2yq)e-U35V2XwtF{W5G&U=L zJ-Z<2&tk1+=}U7og&O_9i9Fy;osl5NZvEwJFOXhn%ye;yY#o}sw&@oVoc|yi{7s&F067nC4Rr`z~xv3Z&&Ji{^Zu@^;~QfB~=$E_VF+L?RSyk`kL~y zx!yLpd-LmrX3VIYAD;Z{sa)!>CoQs}R}UNtU3H>j* z3pu$N7$#p9mfyVk#JkJFF?+Wg+OLuA^GLkE)OO9~K>J0xJ{-GWC?0-xRbW@K-Rox= zQ#@8HanF?4Y#R}IN6~MlFPrVRSmn6~-y|?!obt!w#L>TNE~ls*$*(H>EpkMpuo{>o O89ZJ6T-G@yGywn+i%sPK literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Mine/arrow_right.imageset/arrow_right@2x.png b/QuickLocation/Assets.xcassets/Mine/arrow_right.imageset/arrow_right@2x.png index dc2c628d4725319c239326203465d4afc451f202..98823358a5998a6f55fcea83ed3aac26f9255bfb 100644 GIT binary patch literal 206 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|l3?zm1T2})pg8-ipS0H`m%9S%`&fK_h_ zH*em&e*HR-4IzOHIDm-s`NpyU_3)Pj`2{oZh*;Y@2ba%3eER(L_rI&698LqJ>^xl@ zLp07OCoEuJ;d?;fRehVH%1?f0$;W>j7oTBtzWIw+;?x^^LB%6G{xK~3$k-y=_%g&w i`;b^>#G+|o3=GS90N8K8FSYCPBf!ajGxZv?qO7q4(aWCa_ikc!6y1;-|qm%%~v&U$?1IM}n~YReO2cja8EB@Zq>US{XU zQnEAU?CX1Xq?7645 zb+^QmPfBO36Zy6$U{dJ;o0Aa%%o&GckNN6~?>u$qp~d0FVdQ&MBb@01NWa#Q*>R literal 229 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw)_A%&hE&{od()BkumVGCqDqL9 zs=`7cwS|0bi{%pgomE2)se~{IMQkd_D~tTJ>90N8K8FSYCPBf!ajGxZv?qO7q4(aWCa_ikc!6y1;-|qm%%~v&U$?1IM}n~YReO2cja8EB@Zq>US{XU zQnEAU?CXSF`@#%m077@c+%C|5pnB-^u%bqX5j!`+qzC|E+@mHwypX%Kv{8 zhzkGT0E*;;M1WinyWsy#paQTgP#1^`WP=4k44@hq8^nM~LDVCZfz`ka`Lf&U%O2}5 zdu+b!vHfF{4?B7U8X z`h70u&&9+)7nA;8&HQ&g=iiOIe;_jp0@&gWfd02E3GxeOVBz2q5Rs6PS5ep2H!!xg zcXs#o4-St{PR}ncudZ)y@9v*Gd;a3(tJiPefBf|M%hzw;fBgFW=kLEoE}S^Tz`*F^ z>Eak7A$j%S>nNc>hPI3R9$Qvy;kh6b9OyGkXjA9SxofseZWp=#d;j#b_m&pS)pg&u zO#8R+_}{yEu~&Z`;S-;lwZ+<7>((*LfTUcei_7wICq+%Tdhlk@GWTce)-<^6S6l9$ z7BpRSl2qyJU7MvQRBNWYOnqL&7E-B_-pdu}?AOiJ7^GNt+l%*O^`a|maTl9{v@2w_ zE_Zx*sV)BCa$p|&{AESCHpg~dJ@WJO<1b%+zt`-H747MqEBXAg$nw=qj0S%bgq~Y? zYelfH^LeFyZDLo{fwS(DcN~yfp~A9n<`L6tyS&;Cv=;UizFWD3J!Zf8HOXtyQ`ZXo zPFkbAMqDe~<-pR;xesiv_JkkN+h6z3^l0S*N$V*sre`yPjjpV5^)fSg-mxh4?9Igr zYckJivHt#4;j6bxzyF%Gk!_`|%wwN=lRH87O2LnAD^Mb3@O1Ta JS?83{1OOpIh}QrB literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Mine/clothespin.imageset/clothespin@3x.png b/QuickLocation/Assets.xcassets/Mine/clothespin.imageset/clothespin@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..343f808a172e655d9ae4c29ae00a78ad811b6f89 GIT binary patch literal 1566 zcmYjQ3piA17#_EhYAYGG$>g4iQSGMOMj3Y!lN!53O?G5lX3QWY+ar;XbfJ`IN?K)m zQgTgJN~uj+GlsbsMx{a*sbw%T{QI97wVmfX-+BM{yx;qM|8vftL-O}ptb@@(p-_u` zyxjxgb3MGhwKZVQt~|N|g;LKV`R(+8OMqPI;5r!qGH6K$9s2;-2kPlSOa}w$V2}m4 ziJ&_g2$R476Ldv^mnoo!1q3lblz?dZ5&(#_zXlZV1aHr=%E841aUwFg9Uoy06!iG7(fIA`ZIx;34{zFWP(0e%Rn5&kcYis zm$*3$Ng*s}B2`EW2w*4~D1!i&tK6Uraexd2FqChh7|I~8ngNOt3}2}L6B$Egi0GAA zr9ouM)`;g<9@<rrZ&tS8n9{LKU9NEMz>D)!eAcpa-^U98MXCoIP?5MbSx7z|7gJ;8GOaa0T}_ zMHf}Ui^hC|_qi6Xsv;5du0Sx*q@?~KlUutlz_f7aCUQG=2-+@w7*U-?^ z(waYCd%?m*iEd+@L}GDYzeFN=`D*a>o1u|+?>~%w`FDI=Iyp5h zlgZ_Znc2^!yUgJcD)VtC?)-7Z#|IH^%hgcxUKr?M9jn%(&`o(Ogom+|@MOEB2K8{K z?JNzgq8*Rc`h5uUlMI-N`{~I|;{NHjbuz+KWM&(`;J%S}U*Q3!;19>Dir&Q32jOeT35nc6M&4gF5IkXDw}tUXA6j12x5thp@Ipgu z6?|@}m7XWB4~MID4%lF@i+Gucd2U!my!;k(A^3;#v0z)Ck=4Tl8otS5r=dSaynTrk zR~{AXquITwi<5QRgjC|pNV%dHuEtMP?_~paQ(9bd~BNtVV&)qC(GEE6Ps9H%NPjP6)58}7vX=N=b zjLi(nCy%G#rov`ipQg~r7kvuOd1Q35Kk2?>$J1?iI4NUQvQ{uR z#y2P^`-IRhm0&=%^IcLTHxg>+EiL zh7&E<3VRtj{Q1hSV|Tt}R7aR_8nSi|T-s+ zsJ2Y!xL;)Y1{+Gn=vj5E#?yQ8=7;8^L)Rn~Yv%-C*m^#p6#Ld{{{VBCT5m0GjEgtF maeL!CwmmJ~3 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 literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Mine/clothespin_green.imageset/clothespin_green@3x.png b/QuickLocation/Assets.xcassets/Mine/clothespin_green.imageset/clothespin_green@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..d67cb39c9ca2e5fb8ab3a2f770fa0ba6e6aa5ee6 GIT binary patch literal 2235 zcmV;s2t@aZP)@6@PDb@6KoE;@FPesx%T&2twi`sssq32$2eKoYoS5kf@362%-L!3V}os6r{?a zP$Bw9kq{J>3ZZ<|B`$(0VEbqwRpdaSqAG3bxcO|Fq^X_r`E1|Cx!c`&eY5sm?{0VR zcF))NDrGZsGxO%XH}5xZzGDF&^6xf))pp=*4}1_FI8KB5$V}he&rj{X_I_kE(f4Zz%QunWF$8*;2=?nby&~F^2gc_5KUS=$mOOV(%jnFWL8TiIDo)mA z{Tf-+d*t%Y=yDlc0UTxH4I!UJgnevuD9@}?1Y2eoSoB;xHE^$@)e@EQGGu)p0ZV$c zsxE}CSX6u}&*uQ3hKG*Uf7JaL?Fm-*?B7eTR9a8D@OFte|9G zaoKm^cH+g%%GlyEkRXGtmnB4h1q~2*WuosO$)OO6)A8h4OK93FAmIrh*u(XAuqusX zt$`><|11`E0x-p89tMRmR?OT0`KuV!-YT}k2|&;}Kui}iHBf#sC(MOndmI3EbR50{ zL~j%`HAfGaOB&4X zX$RN21cJ0~;aQiO0;wthG2i(=o(GA7giFS>vM_h^8YD6oA$j8xq!zCh#Ve3VUx1m+ zTad{n`KlpZG4M6J!rtPnJT3Zvd7h&j69@!7dtVEhwy%QD8&W!IBX4Moa5ERVT1& z%Qt{@d+r+d%7hXsk4<{a*E~?hYohYQH1Yj|m*w-@@I}Y6UEZ!L`v$NJhb?{{e@O|I zM??XU(3*0xqwL310%6vY`l}9M4}xz1w!9?IrIzaws^SS(lwPpqxbEzpM5x}gB zn7$tq0s)}8lsXe-N}F6Ng|!xlhSx0>%6^eOv?j71Dgi8rAiQ>e>>1A+Sb=Xj69|C1 zC$kj_#IpGN39^xEo9?thXd-0K34uE3A_A?^yPzplz4w7X$;WrBaQj}Ns6kuwZs=&)$ms@XYu>>5k3usKF8~BTpB4_O2Q@#eHcwyxB&Fv(ur>0JF2bND zbwqCgjo+hgQO9gL|iAX5)dL?7s>H-5$D-ln}N#F~Io5EUYNI02UH0FZJ z2L`a*z$AOY*I8wm-%m2x*=2e@V0kI134n|k|3`!r)ahf7E+WX6zlg(*<9f9wY67q$ zHhdibpN2YpyzE>9P4jhuDQ~hS07{7=vxoTPSf61e=~(x=gBO+Fps{8E_PZ&fo{ndRx3?WS(A|0XulKkA@l@|!LlvLt^E!g0bq5fn@p|yy z22hV)T#1g`9#~25)kF3}?}7IK00960z@PG<00006Nkl90!aX=wle002ov JPDHLkV1l~^FdqN_ literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Mine/clothespin_pink.imageset/Contents.json b/QuickLocation/Assets.xcassets/Mine/clothespin_pink.imageset/Contents.json new file mode 100644 index 00000000..ca44a694 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Mine/clothespin_pink.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "clothespin_pink@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "clothespin_pink@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/Mine/clothespin_pink.imageset/clothespin_pink@2x.png b/QuickLocation/Assets.xcassets/Mine/clothespin_pink.imageset/clothespin_pink@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fa0826eaf31c5949e345d183ff28d500d264df0e GIT binary patch literal 1110 zcmV-c1gZOpP);(AeU-n@7dQO_YdVk8GW2zn3{oP**)!Gi=8lqBj8@ZeEU zK{0}YpyC#E0)ebbFo~IDdZzqd(>>|x>YlC%G`S?aFIDxn`s!76cO@1r^3M{1)GHt# zK0GLUdhU2=lil4-!Cg<404BXjmC6fDc+SIP65=^XtjE}uNCL8}YdL}s%ii9j^4`6* zu4kRU5PuUP7lE)G0EyeaQX_!C)70EN28X94EEv9W;oRX9gY4z(-CP=ze_ z6atv&FqHR!;6B+H-$K$GXcGlFE`xYJ!IASbNFjirc-B&0Lh^2(IeZm?z6o|N?KQ;^ zJt#sw0cZqvv9bno*uRgcMyGuAXs^uYZL~WvaU1k+5O*Fhkv?(i6qC>hxU3Q~Y(k0e zQn~zrT3btKzrz19un#bmSH5F-3*PyLl-n16{xE^6!FNcZb!d9}t)DkcpwBm?*uE2I z&rbRI6#^oc`wWB0Ie^=OrMmx|K_S2jAl)hmkw8F*G|z!U#o}u$vMPE7gh;ajUqxr< zcy)GkbOMu;@1b{qG(@YeM!-9v>AVj@`_C)X2(S`JXB#S&m&mP190BE|DiI4G^yB?M z91f2rAf<9rgF)q~NtzuTpqZaPToeoaR2m;4S*ZlwqWaqYvr{Aik7++U1Ti-{O#i-o zq`9F%a(<7xD2SWvI z6>uX%jHQDovs`vfala~avvHCr=*lX62m2t26^GdIW+p@9(JG zc(-H%rBd|@9iJw~VsO1nBp}Y58AN&pnmQ%)HjRo-B;W--^V)iv2`Us4t7k^QvhLP1 zv*+8BwEyg9L_nN4!7EW7M-Hw5ha9J?hP`SsBH(&Uc^s|jSvW5`I{Qv*}07*qoM6N<$f=w0r^8f$< literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Mine/clothespin_pink.imageset/clothespin_pink@3x.png b/QuickLocation/Assets.xcassets/Mine/clothespin_pink.imageset/clothespin_pink@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..768c42811337d9945934b0775ef0b2d350a05889 GIT binary patch literal 2070 zcmV+x2VpN21U^XAMuCAAI2!rAI6aQ!w?}*o(V}w&_s-)38@B&kslZm zMFc`A7!6?5s0j)YLqJg?K`2n(6hsipv%Bqfw=>u8?9MjRo!6b+LYIWiJ#+7zd(OG% zyU#PX)#wKQ-2^bu19)=;uEhw*En5t^cJ1AA`SRQ4s#O(1ZIs;zz~Psvt?gUt>DfxL z*mlyi9pJqqo11U*0#Y^r4j@Y@nS8)05DEZ8cdu+}ng?2h^1Ep4F}Y;PAg7U%%Z4!@ zJv|e4!t6c*5OX2dn|uMt)vNEqkbQ!oI|%88pt&e7AYI=@H8st0&6@kcD=DEvs?bIw zoM)#37pvV6PoHs^MwRwEc>-YPA@CK@HXx6IoQZYD1Iz|EFxFWSr3~FFGR-&920I@` z^)^hShw}>TOun4Wv(rP$Md+2MvkbonFD}bkiXMEzPb83Unhmz|Re^F|m8N+v&vpTz z>gq>P`7B5!jm9>&9MkCq=dk)IN+|_Kw+^7+hw~pnF1qtX8H&fdpqou@07PR@Qv*^< zJQAk)HFb7=2*(~`Cw�nx4kUu7LAz`^tn@>=yO)0VAMk3`&IC=_65%x7l#)dunc8 z3?Q$99+HiX8hZS3%=g!ke*u~GpmCtWbp@7k`pGHD&N|--i!%d9tiDX8H{K!cM-RUS z{VzcWXyix}@4H}gG4hGv-407Ehpeb1vG9L!YPtasokC;1bId7R2IbQTFHSi9a07_N zjzAy*D!(K|D;Y+I(=itSF=^625ZDJQ$8><&+k*jQy%u6yHZQM73TtV6U8* z?8@zSUQXFnIOCcbo&dy@DLeuN3mNJIo37%}JXYv}CjeXMS3574tQ8%5y3m;$34mvZ z`U8-13()p}${|@ez&3k1;twDf@%1@0ybN3?UK>K_RE1YjQ=1Gx_Pd*To^G$j2xD-eLK#9@V9yv~^r z-1B>c5Yn&ZZ~&I&d4Loel$J?WcZzy0U!wH+bG?*lKTl>7J8~L9r1bTp&|L0a$^>cJ z;oOV&GaNu7@fTFvLB7dKby4@pwiGQ-4mG8W*0si(8H)#N<7_oy(kEGBY^uoOim4erk#YLHjqb(x-elvO=YW?F{1<0 zfk_Pj(N)~(9B^5}^_S2M7dwa{bX*H*Pz(~v)osmX!A~^+i9`YmkFQ$_tO?SqhLAAS zLFhxON$8>SJ@|m%L<&D80Akjx3;=LohY8=;s$Qk!kgX(C0k_ehL1#(frvxBtN7p%| zaK%L;79*{4a8{)E;ZqQXP8vqcr`pNp&bsPk_?0!k3Kw6~_~XU}faQ$3TR96QVJLx> zSa192&O1XFFgK_UARa#h1@0MSeUmYC7}i1fg|jwz2x;-KKZ6l}e;f@v!15j_0}$iK za}2N}ujtH$rjb!Ql!AYPVs&-FBDva27=hinV)#=TK-QFLdR}dCoiPm42tc^(MZEhM zwYPXx74k0O3rzVkAsT>Wl4GJ|VT=5(SJzN%*l-j*_IO>V_#Jl<38m)t(1ZyWNb#p+ zF>fm62BRqB0&RC2Qji2w7x&eo>h?n*j8GRE5$01Q%$W@TdNen zISm#Ez%>8Ha9?&BDzV&!aD6Aj6<4?(X16E+V)}I6iB^Oek5GSxG;LD^`CieY0NC1V zk@wd$+AQkpkHcQ%R5Sql-t_axExHww_j9eTiYABNqZiS@ovHk@;}t^zRuqdmr&+^BH0VDvuW_) zC*k-;PzwB#QojIT4>$p zcu~Ir$Q$QSga2g*j&4H_uNPCNHlzFlMyW+ioY#w}u0rZz+s&;MyQ0=K}C3kZK zu2s*f+P&dN;C}!B0RR7q^93IO000I_L_t&o0B4_guSZiBo&W#<07*qoM6N<$f{Lle AvH$=8 literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Mine/customer_service.imageset/customer_service@2x.png b/QuickLocation/Assets.xcassets/Mine/customer_service.imageset/customer_service@2x.png index c2a55286b659845e0a97806b3909eaa827b95264..4dee707330e774e29906b7a1db0de55fcf23d432 100644 GIT binary patch literal 1908 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 literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Mine/quick_data_security.imageset/icon@3x.png b/QuickLocation/Assets.xcassets/Mine/quick_data_security.imageset/icon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..810eb3db8e32ac6fbd54f85ac96b1a12e8cb06d6 GIT binary patch literal 2971 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 R0?_~f002ovPDHLkV1lE?o5uhE literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Mine/quick_permission.imageset/Contents.json b/QuickLocation/Assets.xcassets/Mine/quick_permission.imageset/Contents.json new file mode 100644 index 00000000..6402a56a --- /dev/null +++ b/QuickLocation/Assets.xcassets/Mine/quick_permission.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "icon@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "icon@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/Mine/quick_permission.imageset/icon@2x.png b/QuickLocation/Assets.xcassets/Mine/quick_permission.imageset/icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2abb2551c1df538f24fb96662e5e5062d0442085 GIT binary patch literal 1962 zcmb`Ii&Il)8ixahLa}tsO1rc=7O<@gO34-}A|c@(qL73@xfG!(5`jpycgg_uxm_R=QxWbK>#V#<3&pzyhKul%$pfG=M3t=!I41*VlRLYP_ZEbBG z9Ub9txQmO6tE;P763j14@$WNyuvhUsnKb!I?-TGJ*3GDE|W-61rlb zsqZyzFhW;0BUgbe_0aZgV}z}2hOHO`x(h~E=))A7VG2XILL08oMJhCrO5g{j`jk=? zt5n1)l|PfO{imIBRswPuaW2vf%RP|W8 zTAr>R$yCcS)xXiy!!)(*yn67wdWf!;($$06>f70XBo`3p0)4rFgaPz20C67Bod<{t zfX*U7SOi=z0oqD5EoB-(nWm*e(^#QtVruwIO(RRgV`=KynmV?I$I);(+Uja8yIRZP zYFS(@yH3mGY0G%pih6BPy|#p}%WKr-HR*B%I(mza#!z;if7Qc$#RMNYWO|uADeUc;K8#0__VPT(-X#8mUc83~}IigI$qNvWN5&_RMOHzqco z1E(3i?lP8J6<%9L=8<EO*xU05Rp!ZP7eYr=)`z1cL*T{U}8eR+o7ZJm}o30sIYKj;zGO#1QebCx-aeo zm_8>0!1Tfdcwx||xee#`BDT#dWFWgYx8w3)j1P z25K6RPLM+R5j@fonsBvI^1kaEacN){^sR(2-kT6)n{8_mP#H+G3HVE)$z+RzncifG z`_S(1q6e|3->Cbk?%j5z3 zAaFqCI1~o^?ZC|!c-phm57}Fm3SpGpi>6U?bH=5ia`6@CcvHR87RJjTagS=lx3?!C zKK>#$b*paj(3#TG?>McZPsaDov1SY7U@-T0?i+f$80|w(?J*epK&v&3?buqr`-FYe zSZ%vvZfK?<3vlksoOeKfx&Dip=wG3J@i#gY{JRpsIks#4&G1QVutRIlwd^Y$iubbh zt$g{XPO<4P-&D(5{#rG_2)ys!zg7qAz16*3E|`)>{N907wc3o;-%8rm{NNw`38T-r z^L%to_;h<~%+&JjclRQG3h#MV_gM-GY2)0mR(Ebfaq(`%l&gGYW#4TlrPedW=Z^nt zlb5EapBh&55 zNU^$@Cq|?O&NBKNJt`}y6h)*sZSs|q_<8r{?#IGCA03h|Eo!DIPqXEJ%-mEx*4Y%@ zT(Btp-C10=Rh973qWMFtB+(}8!%>OT1`O;

+%I_1usxyy3z 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 VXpWov6g2<<002ovPDHLkV1k&(mkIy? literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Mine/quick_privacy.imageset/Contents.json b/QuickLocation/Assets.xcassets/Mine/quick_privacy.imageset/Contents.json new file mode 100644 index 00000000..6402a56a --- /dev/null +++ b/QuickLocation/Assets.xcassets/Mine/quick_privacy.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "scale" : "1x" }, + { "filename" : "icon@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "icon@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "author" : "xcode", "version" : 1 } +} diff --git a/QuickLocation/Assets.xcassets/Mine/quick_privacy.imageset/icon@2x.png b/QuickLocation/Assets.xcassets/Mine/quick_privacy.imageset/icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..01b1de420cb862f3988e7a77e3d880fda9d287be GIT binary patch literal 1615 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 literal 0 HcmV?d00001 diff --git a/QuickLocation/Assets.xcassets/Mine/quick_status.imageset/icon@3x.png b/QuickLocation/Assets.xcassets/Mine/quick_status.imageset/icon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3201386a5b5a3676fc613cabaf4b927142f4712b GIT binary patch literal 2727 zcmV;Y3Rv}tP)8gBh-fMtpdzJ+N{vYn9i)inqlWr} zXn+!e#yDx3AKNmCacbJoM6Gd*%}~>5>nJL1(X{*==W$+6MBKjb-U@endqi@39KwAw z@7&ED_wBdOKJU->e(y_ISZIVsXpHAEB_ZMKkp}L2s#w!>jh)dpr$C$_yeR!zJ$Ft^GxZeB%t}m5{K|yGCxSLTPU+dx0LFDU zU?jo~!0UfBI%era!X}*3o8sWculNQ=MhwO*WZk$|E3J;1Au@*_kdcI&q|*SG;jH9hG84bA;rt7m#g4vJ6t92m7(m5}tn zZ3_%~9!h6=L~CIF;_^zv-tY|&I~rsZP(kbP#(PkXXdx`P?Ytb?3G_VT4g0#etH7$a zAof|VG8heNeX?2|$NdmR8_zz3pW$~}1NWxD7~k`BfjoH|*(Ndx13n)rbsbdWBFOq5 zMRWJw>WueAbKihD%g|869DUyNGQE_J&ABZ=q6|!?_*A`a0&cPrgTpWBuERlPr)g=f z+}Hfbq%o~-81;fmw0Xxcz#1KwrNk4)IlzmtK0*cXkVa-~f|Fpn8y46kv*XQuIbd!OzD^c2aTw=#}aJL z0yNrz4M5r1-VTB39f=bN_z*mdchvi9^~||5oyGN<{)2)0gfU*LBSnmyt2o3;~ZE=tsi=AYhIlqa{Uiwc->a{g!_D8yNhJ;n4 z5_N?st^8yFAWmh8R=Ncg96Zj23t7C%P9W9kFMH;9 z^v`tnR;W%ljc_i2GXxB>rcGeYN@R`A#hNv?7E)q6PD*V@NxA(nS<<}gh3Su{^?210 zL6Leuz`>&&R{^D`YC-H%=db57ck%?A7XgxLQ?D(rtC8}0i@(b2kE9XGE-0+1Dou%1v99ApSaq+(HmJ7tTMC=f7r zIUjQY;~0!-@~uNgc!irf6%Voo)#;c3Ot}C|vk1(7vdQrl$nVHz`|A~1RYh-03r4{f zy{2ek1zLZxpTga(f27p>i+Zjs^iU@mBV1(lWR1Hz5&OFU&VEv9M_`)B7RO#v<#>(k zaJ)il9J|TG8R@MO=AXqgJ*A^&r=+}Ofl~A5^z^J0)X5^JPTodXPMs>IWssW-qgD5d7l@98XcsZ;xq5w@KcfGHM$`B((TlQpPL zm7+RT0XWr;R{@wkq{fNhGyphtq|sSJ)*ULX59om+6>FrV#o($~)~M9{z?x@0SOe-* zggt>l)@*US;l>)5IyDHa*#W>*k==cp$o6yPTjNvbT$HT+1hz;h29Ru- z3vMs;+y^Qs{q?sF5!C6aB8lsESEmX9XGooz1YoKJU|tb`LDn2?1Go1hy2!viDC(Fk|2teyejASwG45Fv2z& z*4Vv`u!}YITr|c8Agx%HYdHc?a%Cp7*m^I7B!(_pJYL_F6o%?-h(x*{NvG>&MY+?RPyT0!AMuMdO&65Wz8*}Q{?47%A zS+lWVQOm5%^fR%>R7>=uDZK~?<-MJ_8SmWi+=sCh0dEv`wF0n<#lX=jwlewO0~XI+ zk9s84j)LzM>MO&w*?={l;F?FZuoRy@?rDG(RhY38`Z1JinDbX~&v*#eM(LQ$(WuuR zmQiS9u-XixYmnE0${nK(3S9RB$UcpZ&BGuT%)z~5xIKzY!s8!ojRJaCi+x@?-7zgiWL& z1e$te6BuFnAQRd-*>_-*TsrOy@~vm<&39l!UX { .init("isShowGuide", defaultValue: true) } /// 气泡持续时间 var bubbleKeepTimes: DefaultsKey { .init("bubbleKeepTimes") } + /// 解锁统计:今日次数 + var unlockCountToday: DefaultsKey { .init("unlockCountToday", defaultValue: 0) } + /// 解锁统计:累计次数 + var unlockCountTotal: DefaultsKey { .init("unlockCountTotal", defaultValue: 0) } + /// 解锁统计:日期 yyyy-MM-dd + var unlockCountDate: DefaultsKey { .init("unlockCountDate", defaultValue: "") } + /// 上次锁屏态(杀进程补计) + var unlockWasLocked: DefaultsKey { .init("unlockWasLocked", defaultValue: false) } + var unlockLastCountedAt: DefaultsKey { .init("unlockLastCountedAt") } + var unlockLastHeartbeatAt: DefaultsKey { .init("unlockLastHeartbeatAt") } + var unlockLastLatitude: DefaultsKey { .init("unlockLastLatitude") } + var unlockLastLongitude: DefaultsKey { .init("unlockLastLongitude") } + /// 手机报告:按自然日累计屏幕时长(秒),key = yyyy-MM-dd + var screenTimeByDay: DefaultsKey<[String: Int]> { .init("screenTimeByDay", defaultValue: [:]) } + /// 手机报告:进行中的解锁 session 起点(持久化,杀进程可续) + var screenSessionStartAt: DefaultsKey { .init("screenSessionStartAt") } + /// 手机报告:本 App 今日进入前台次数 + var appUsageToday: DefaultsKey { .init("appUsageToday", defaultValue: 0) } } /// 通知常量 @@ -43,6 +61,8 @@ extension Notification.Name { static let resumePopupQueue = Notification.Name("resumePopupQueue") /// 终止弹窗队列(登录成功后) static let invalidatePopupQueue = Notification.Name("invalidatePopupQueue") + /// 本机解锁次数变化 + static let unlockCountDidChange = Notification.Name("unlockCountDidChange") } diff --git a/QuickLocation/Manager/Unlock/UnlockCountManager.swift b/QuickLocation/Manager/Unlock/UnlockCountManager.swift index 719939aa..c6edee4c 100644 --- a/QuickLocation/Manager/Unlock/UnlockCountManager.swift +++ b/QuickLocation/Manager/Unlock/UnlockCountManager.swift @@ -31,6 +31,8 @@ final class UnlockCountManager: NSObject { private var lastGeofenceCenter: CLLocationCoordinate2D? private var lastKnownLocation: CLLocation? private var bootBackgroundTask: UIBackgroundTaskIdentifier = .invalid + /// 当前解锁 session 起点(锁屏结算屏幕时长) + private var screenSessionStart: Date? private override init() { super.init() @@ -74,6 +76,23 @@ final class UnlockCountManager: NSObject { return Defaults[\.unlockCountTotal] } + /// 今日屏幕使用时长(秒),含当前解锁 session 的今日部分 + var todayScreenTimeSeconds: Int { + rollDayIfNeeded() + let today = todayString() + var total = Defaults[\.screenTimeByDay][today] ?? 0 + if let start = effectiveScreenSessionStart { + total += screenTimeSeconds(from: start, to: Date(), onDay: today) + } + return total + } + + /// 本 App 今日进入前台次数 + var todayAppUsageCount: Int { + rollDayIfNeeded() + return Defaults[\.appUsageToday] + } + var hasAlwaysLocationAuthorization: Bool { authorizationStatus() == .authorizedAlways } @@ -95,6 +114,7 @@ final class UnlockCountManager: NSObject { } didStart = true rollDayIfNeeded() + restoreScreenSessionIfNeeded() let available = UIApplication.shared.isProtectedDataAvailable isSessionLocked = !available @@ -140,6 +160,9 @@ final class UnlockCountManager: NSObject { restartLocationMonitoringIfPossible(reassertContinuous: true) handleNonActiveLaunchIfNeeded(launchOptions) reconcileUnlockIfNeeded(reason: "coldStartAfterLock") + if UIApplication.shared.isProtectedDataAvailable, !isSessionLocked { + startScreenSession() + } Defaults[\.unlockLastHeartbeatAt] = Date() } @@ -162,13 +185,120 @@ final class UnlockCountManager: NSObject { @discardableResult private func rollDayIfNeeded() -> Bool { + migrateScreenTimeStorageIfNeeded() let today = todayString() - if Defaults[\.unlockCountDate] != today { - Defaults[\.unlockCountDate] = today - Defaults[\.unlockCountToday] = 0 - return true + guard Defaults[\.unlockCountDate] != today else { return false } + + flushScreenSession() + + Defaults[\.unlockCountDate] = today + Defaults[\.unlockCountToday] = 0 + Defaults[\.appUsageToday] = 0 + return true + } + + private var effectiveScreenSessionStart: Date? { + screenSessionStart ?? Defaults[\.screenSessionStartAt] + } + + private func dayString(for date: Date) -> String { + let formatter = DateFormatter() + formatter.calendar = Calendar.current + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone.current + formatter.dateFormat = "yyyy-MM-dd" + return formatter.string(from: date) + } + + private func migrateScreenTimeStorageIfNeeded() { + var byDay = Defaults[\.screenTimeByDay] + guard byDay.isEmpty else { return } + + let legacyKey = "screenTimeTodaySeconds" + let legacyToday = UserDefaults.standard.integer(forKey: legacyKey) + if legacyToday > 0 { + let dateKey = Defaults[\.unlockCountDate].isEmpty ? todayString() : Defaults[\.unlockCountDate] + byDay[dateKey] = legacyToday + Defaults[\.screenTimeByDay] = byDay } - return false + } + + private func addScreenTime(from start: Date, to end: Date) { + guard end > start else { return } + var cursor = start + let calendar = Calendar.current + var byDay = Defaults[\.screenTimeByDay] + + while cursor < end { + let dayStart = calendar.startOfDay(for: cursor) + guard let nextDay = calendar.date(byAdding: .day, value: 1, to: dayStart) else { break } + let segmentEnd = min(end, nextDay) + let seconds = max(0, Int(segmentEnd.timeIntervalSince(cursor))) + if seconds > 0 { + let key = dayString(for: cursor) + byDay[key, default: 0] += seconds + } + cursor = segmentEnd + } + + Defaults[\.screenTimeByDay] = byDay + } + + private func screenTimeSeconds(from start: Date, to end: Date, onDay dayKey: String) -> Int { + guard end > start else { return 0 } + let calendar = Calendar.current + guard + let dayStart = date(fromDayKey: dayKey), + let nextDay = calendar.date(byAdding: .day, value: 1, to: dayStart) + else { return 0 } + + let segmentStart = max(start, dayStart) + let segmentEnd = min(end, nextDay) + guard segmentEnd > segmentStart else { return 0 } + return max(0, Int(segmentEnd.timeIntervalSince(segmentStart))) + } + + private func date(fromDayKey key: String) -> Date? { + let formatter = DateFormatter() + formatter.calendar = Calendar.current + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone.current + formatter.dateFormat = "yyyy-MM-dd" + return formatter.date(from: key) + } + + private func restoreScreenSessionIfNeeded() { + guard UIApplication.shared.isProtectedDataAvailable else { return } + guard screenSessionStart == nil, let persisted = Defaults[\.screenSessionStartAt] else { return } + screenSessionStart = persisted + } + + private func startScreenSession() { + guard UIApplication.shared.isProtectedDataAvailable else { return } + rollDayIfNeeded() + if screenSessionStart == nil { + if let persisted = Defaults[\.screenSessionStartAt] { + screenSessionStart = persisted + } else { + screenSessionStart = Date() + Defaults[\.screenSessionStartAt] = screenSessionStart + } + } + } + + private func flushScreenSession() { + guard let start = effectiveScreenSessionStart else { return } + addScreenTime(from: start, to: Date()) + screenSessionStart = nil + Defaults[\.screenSessionStartAt] = nil + notifyChange() + } + + private func recordAppUsage() { + rollDayIfNeeded() + Defaults[\.appUsageToday] = Defaults[\.appUsageToday] + 1 + notifyChange() + debugLog("appUsage today=\(Defaults[\.appUsageToday])") } // MARK: - Counting @@ -183,12 +313,14 @@ final class UnlockCountManager: NSObject { isSessionLocked = false Defaults[\.unlockLastHeartbeatAt] = now stopProtectedDataPolling() + startScreenSession() notifyChange() print("🔓 UnlockCount +\(reason) today=\(Defaults[\.unlockCountToday]) total=\(Defaults[\.unlockCountTotal])") debugLog("recordUnlock reason=\(reason) today=\(Defaults[\.unlockCountToday]) total=\(Defaults[\.unlockCountTotal])") } private func markLocked() { + flushScreenSession() isSessionLocked = true hasSeenLockInSession = true Defaults[\.unlockWasLocked] = true @@ -344,6 +476,7 @@ final class UnlockCountManager: NSObject { debugLog("didBecomeActive protected=\(UIApplication.shared.isProtectedDataAvailable) wasLocked=\(Defaults[\.unlockWasLocked]) sessionLocked=\(isSessionLocked)") endBootBackgroundTask() reconcileUnlockIfNeeded(reason: "becomeActiveAfterLock") + recordAppUsage() restartLocationMonitoringIfPossible(reassertContinuous: true) Defaults[\.unlockLastHeartbeatAt] = Date() } diff --git a/QuickLocation/Model/GroupModel.swift b/QuickLocation/Model/GroupModel.swift index f8a78899..1c779092 100644 --- a/QuickLocation/Model/GroupModel.swift +++ b/QuickLocation/Model/GroupModel.swift @@ -189,6 +189,10 @@ struct GroupMemberModel: Mappable, Equatable { } /// 状态:0、无状态 1、SOS 2、气泡等 var status: Int = 0 + /// 入圈天数(若服务端下发) + var join_days: Int = 0 + /// 入圈时间戳毫秒(若服务端下发) + var join_time: Int64 = 0 /// 是否在线 var is_online: Bool = false /// 设备电量 @@ -235,6 +239,8 @@ struct GroupMemberModel: Mappable, Equatable { is_online <- map["is_online"] last_active_time <- map["last_active_time"] status <- map["status"] + join_days <- (map["join_days"], kStrTransformInt) + join_time <- map["join_time"] } } diff --git a/QuickLocation/Section/Group/GroupItineraryView.swift b/QuickLocation/Section/Group/GroupItineraryView.swift index 43d89ff3..7870d53b 100644 --- a/QuickLocation/Section/Group/GroupItineraryView.swift +++ b/QuickLocation/Section/Group/GroupItineraryView.swift @@ -21,83 +21,23 @@ final class GroupItineraryView: UIView { groupNameLab.text = name.isEmpty ? " " : name } - func reloadMembers(_ members: [GroupMemberModel], selectedId: String, groupKey: String = "") { - memberList = Self.sortedMemberList(members, groupKey: groupKey) - // 保留有效选中;无效则用传入 selectedId → 自己 → 第一项(对齐首页) - if memberList.contains(where: { $0.user_id == selectedMemberId }) { - // keep selectedMemberId - } else if memberList.contains(where: { $0.user_id == selectedId }) { - selectedMemberId = selectedId - } else if let me = memberList.first(where: { $0.user_id == AppContextManager.shared.userId }) { - selectedMemberId = me.user_id - } else { - selectedMemberId = memberList.first?.user_id ?? "" - } - memberCV.reloadData() - scrollToSelectedMember(animated: false) - } - func reloadSchedules(_ list: [ScheduleModel]) { scheduleSections = Self.groupByDay(list) tableView.reloadData() emptyLab.isHidden = !list.isEmpty } - private var memberList: [GroupMemberModel] = [] - private var selectedMemberId: String = "" - private var scheduleSections: [(title: String, items: [ScheduleModel])] = [] - - /// 与 HomeViewModel / GroupMemberView2 一致:圈主 -> 自己 -> 在线;同优先级保持接口原始顺序 - private static func sortedMemberList(_ list: [GroupMemberModel], groupKey: String) -> [GroupMemberModel] { - list.enumerated() - .sorted { lhs, rhs in - let lhsPriority = memberSortPriority(lhs.element, groupKey: groupKey) - let rhsPriority = memberSortPriority(rhs.element, groupKey: groupKey) - if lhsPriority != rhsPriority { return lhsPriority < rhsPriority } - return lhs.offset < rhs.offset - } - .map(\.element) - } - - private static func memberSortPriority(_ member: GroupMemberModel, groupKey: String) -> Int { - if !groupKey.isEmpty, groupKey.contains(member.user_id) { return 0 } - if member.user_id == AppContextManager.shared.userId { return 1 } - if member.is_online { return 2 } - return 3 - } - - private func scrollToSelectedMember(animated: Bool) { - guard !selectedMemberId.isEmpty, - let idx = memberList.firstIndex(where: { $0.user_id == selectedMemberId }) else { return } - let indexPath = IndexPath(item: idx, section: 0) - DispatchQueue.main.async { [weak self] in - guard let self = self, - self.memberCV.numberOfSections > 0, - self.memberCV.numberOfItems(inSection: 0) > idx else { return } - self.memberCV.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: animated) - } - } + private var scheduleSections: [(dayText: String, dateText: String, items: [ScheduleModel])] = [] private func setupUI() { addSubview(headerRow) headerRow.addSubview(groupIcon) headerRow.addSubview(groupNameLab) headerRow.addSubview(changeGroupBtn) - - let memberView = UIView() - memberView.backgroundColor = .white - memberView.cornerRadius = 20 - memberView.addSubview(memberCV) - addSubview(memberView) - - memberView.layoutChain - .topToBottomOfView(headerRow, offset: 12) - .edgesHorzontal(15) - .height(100) - addSubview(tableView) addSubview(emptyLab) - + addSubview(createBtn) + headerRow.layoutChain .top(8) .edgesHorzontal(16) @@ -119,53 +59,65 @@ final class GroupItineraryView: UIView { .centerY() .rightToLeftOfView(changeGroupBtn, offset: -8, relation: .lessThanOrEqual) - memberCV.layoutChain - .edges(all: 5, excludingEdge: .bottom) - .bottom() - tableView.layoutChain - .topToBottomOfView(memberCV, offset: 8) + .topToBottomOfView(headerRow, offset: 12) .edgesHorzontal() .bottom() emptyLab.layoutChain .centerX() .centerY(tableView) + + createBtn.layoutChain + .bottom(kSafeBottomMargin + 102) + .right(10) + .width(60) + .heightToWidth(1) } private func setupRx() { + createBtn.rx.tap.subscribe(onNext: { _ in + AppRouter.push(Route.createSchedule) + }).disposed(by: disposeBag) + changeGroupBtn.rx.tap .subscribe(onNext: { [weak self] in self?.onSwitchGroup?() }) .disposed(by: disposeBag) } - private static func groupByDay(_ list: [ScheduleModel]) -> [(title: String, items: [ScheduleModel])] { + private static func groupByDay(_ list: [ScheduleModel]) -> [(dayText: String, dateText: String, items: [ScheduleModel])] { let sorted = list.sorted { $0.timestamp > $1.timestamp } var map: [String: [ScheduleModel]] = [:] var order: [String] = [] + var dayDateMap: [String: (dayText: String, dateText: String)] = [:] let fmt = DateFormatter() fmt.locale = Locale(identifier: "zh_CN") let calendar = Calendar.current for item in sorted { let date = Date(timeIntervalSince1970: TimeInterval(item.timestamp) / 1000) fmt.dateFormat = "M月d日" - let day = fmt.string(from: date) - let title: String + let dateText = fmt.string(from: date) + let dayText: String if calendar.isDateInToday(date) { - title = "今天 \(day)" + dayText = "今天" } else if calendar.isDateInYesterday(date) { - title = "昨天 \(day)" + dayText = "昨天" } else { - fmt.dateFormat = "EEEE M月d日" - title = fmt.string(from: date) + fmt.dateFormat = "EEEE" + dayText = fmt.string(from: date) } - if map[title] == nil { - order.append(title) - map[title] = [] + let key = "\(dayText) \(dateText)" + if map[key] == nil { + order.append(key) + dayDateMap[key] = (dayText, dateText) + map[key] = [] } - map[title]?.append(item) + map[key]?.append(item) + } + return order.map { key in + let pair = dayDateMap[key] ?? ("", "") + return (pair.dayText, pair.dateText, map[key] ?? []) } - return order.map { ($0, map[$0] ?? []) } } lazy var headerRow: UIView = { @@ -198,24 +150,9 @@ final class GroupItineraryView: UIView { return btn }() - lazy var memberCV: UICollectionView = { - let layout = UICollectionViewFlowLayout() - layout.itemSize = CGSize(width: 61, height: 95) - layout.minimumLineSpacing = 15 - layout.sectionInset = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) - layout.scrollDirection = .horizontal - let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) - cv.backgroundColor = .clear - cv.showsHorizontalScrollIndicator = false - cv.register(GroupMemberListCell.self) - cv.dataSource = self - cv.delegate = self - return cv - }() - lazy var tableView: UITableView = { let tv = UITableView(frame: .zero, style: .grouped) - tv.backgroundColor = .clear + tv.backgroundColor = UIColor(hexStr: "#FAFAFA") tv.separatorStyle = .none tv.showsVerticalScrollIndicator = false tv.register(GroupItineraryCell.self) @@ -239,10 +176,16 @@ final class GroupItineraryView: UIView { label.isHidden = true return label }() + + lazy var createBtn: UIButton = { + let btn = UIButton() + btn.setImage(UIImage(named: "Schedule/create"), for: .normal) + return btn + }() override init(frame: CGRect) { super.init(frame: frame) - backgroundColor = UIColor(hexStr: "#FAFAFA") + backgroundColor = .clear setupUI() setupRx() } @@ -252,30 +195,6 @@ final class GroupItineraryView: UIView { } } -// MARK: - UICollectionView -extension GroupItineraryView: UICollectionViewDataSource, UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - memberList.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell: GroupMemberListCell = collectionView.dequeueReusableCell(for: indexPath) - let model = memberList[indexPath.item] - cell.configure( - model: model, - isCurrentUser: model.user_id == AppContextManager.shared.userId, - isSelected: model.user_id == selectedMemberId - ) - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - selectedMemberId = memberList[indexPath.item].user_id - memberCV.reloadData() - scrollToSelectedMember(animated: true) - } -} - // MARK: - UITableView extension GroupItineraryView: UITableViewDataSource, UITableViewDelegate { func numberOfSections(in tableView: UITableView) -> Int { @@ -289,10 +208,27 @@ extension GroupItineraryView: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let header = UIView() header.backgroundColor = .clear + let sectionData = scheduleSections[section] let label = UILabel() - label.text = scheduleSections[section].title - label.font = .systemFont(ofSize: 13, weight: .semibold) - label.textColor = UIColor(hexStr: "#353B4F") + let color = UIColor(hexStr: "#353B4F") + let attr = NSMutableAttributedString() + attr.append(NSAttributedString( + string: sectionData.dayText, + attributes: [ + .font: UIFont.systemFont(ofSize: 16, weight: .bold), + .foregroundColor: color + ] + )) + if !sectionData.dateText.isEmpty { + attr.append(NSAttributedString( + string: " \(sectionData.dateText)", + attributes: [ + .font: UIFont.systemFont(ofSize: 13, weight: .medium), + .foregroundColor: color + ] + )) + } + label.attributedText = attr header.addSubview(label) label.layoutChain.left(16).centerY() return header @@ -340,9 +276,9 @@ final class GroupItineraryCell: UITableViewCell { let date = Date(timeIntervalSince1970: TimeInterval(model.timestamp) / 1000) let ongoing = Calendar.current.isDateInToday(date) || date > Date() statusLab.text = ongoing ? "进行中" : "已完成" - statusLab.textColor = ongoing ? UIColor(hexStr: "#16B3FF") : UIColor(hexStr: "#999999") + statusLab.textColor = ongoing ? UIColor(hexStr: "#00B0C6") : UIColor(hexStr: "#999999") statusLab.backgroundColor = ongoing - ? UIColor(hexStr: "#E3F6FF") + ? UIColor(hexStr: "#DCFCFF") : UIColor(hexStr: "#F0F0F0") let tag = model.groups.first?.group_name ?? "" @@ -420,43 +356,45 @@ final class GroupItineraryCell: UITableViewCell { private lazy var avatarImg: UIImageView = { let iv = UIImageView() iv.contentMode = .scaleAspectFill - iv.cornerRadius = 18 + iv.cornerRadius = 10 iv.clipsToBounds = true return iv }() private lazy var nameLab: UILabel = { let label = UILabel() - label.font = .systemFont(ofSize: 14, weight: .semibold) + label.font = .systemFont(ofSize: 14, weight: .bold) label.textColor = UIColor(hexStr: "#353B4F") return label }() private lazy var statusLab: PaddingLabel = { let label = PaddingLabel() - label.font = .systemFont(ofSize: 11, weight: .medium) + label.font = .systemFont(ofSize: 12, weight: .medium) label.textAlignment = .center - label.cornerRadius = 6 + label.cornerRadius = 4 label.clipsToBounds = true label.insets = UIEdgeInsets(top: 2, left: 8, bottom: 2, right: 8) + label.borderWidth = 1 + label.borderColor = UIColor(hexStr: "#84E2ED") return label }() private lazy var summaryLab: UILabel = { let label = UILabel() label.text = "今天停留了" - label.font = .systemFont(ofSize: 14, weight: .medium) + label.font = .systemFont(ofSize: 16, weight: .bold) label.textColor = UIColor(hexStr: "#353B4F") return label }() private lazy var placeCountLab: UILabel = { let label = UILabel() - label.font = FontManager.youSheBiaoTiHei(12) - label.textColor = .white + label.font = FontManager.youSheBiaoTiHei(14) + label.textColor = UIColor(hexStr: "#58EDFF") label.textAlignment = .center - label.backgroundColor = UIColor(hexStr: "#353B4F") - label.cornerRadius = 11 + label.backgroundColor = UIColor(hexStr: "#293445") + label.cornerRadius = 6 label.clipsToBounds = true return label }() @@ -464,7 +402,7 @@ final class GroupItineraryCell: UITableViewCell { private lazy var summaryTailLab: UILabel = { let label = UILabel() label.text = "个地方" - label.font = .systemFont(ofSize: 14, weight: .medium) + label.font = .systemFont(ofSize: 16, weight: .bold) label.textColor = UIColor(hexStr: "#353B4F") return label }() diff --git a/QuickLocation/Section/Group/GroupViewController.swift b/QuickLocation/Section/Group/GroupViewController.swift index c3cd9d5e..377861ec 100644 --- a/QuickLocation/Section/Group/GroupViewController.swift +++ b/QuickLocation/Section/Group/GroupViewController.swift @@ -210,11 +210,6 @@ final class GroupViewController: BaseViewController { self.itineraryGroupModel = model let current = model.groups.first(where: { $0.group_key == model.default_group_key }) self.rootView.itineraryPage.updateGroupName(current?.name ?? "") - self.rootView.itineraryPage.reloadMembers( - model.select_group_employee, - selectedId: AppContextManager.shared.userId, - groupKey: model.default_group_key - ) self.requestItinerarySchedules(groupKey: model.default_group_key) }).disposed(by: disposeBag) } diff --git a/QuickLocation/Section/Group/InviteJoin/InviteJoinView.swift b/QuickLocation/Section/Group/InviteJoin/InviteJoinView.swift index 53483a0b..5ba372dc 100644 --- a/QuickLocation/Section/Group/InviteJoin/InviteJoinView.swift +++ b/QuickLocation/Section/Group/InviteJoin/InviteJoinView.swift @@ -131,8 +131,6 @@ class InviteJoinView: UIView { lazy var navView: BaseNavigationView = { let nav = BaseNavigationView(title: "邀请成员") - nav.backBtn.setImage(UIImage(named: "Group/invite_back"), for: .normal) - nav.backBtn.layoutChain.width(36).height(36) return nav }() diff --git a/QuickLocation/Section/Home/GroupMemberView2.swift b/QuickLocation/Section/Home/GroupMemberView2.swift index 6c14eb02..d954d74f 100644 --- a/QuickLocation/Section/Home/GroupMemberView2.swift +++ b/QuickLocation/Section/Home/GroupMemberView2.swift @@ -8,13 +8,12 @@ import UIKit import Network import CoreLocation -import RxSwift -import RxCocoa import SwiftyUserDefaults class GroupMemberView2: UIView { private var navBarHeightConstraint: NSLayoutConstraint? + private var currentMemberModel: GroupMemberModel? private static let batteryNormalColor = UIColor(hexStr: "#47DD00") private static let batteryLowColor = UIColor(hexStr: "#F85A5D") @@ -28,6 +27,7 @@ class GroupMemberView2: UIView { } func setupMemberInfo(_ model: GroupMemberModel, isOwner: Bool) { + currentMemberModel = model memberNameLab.text = model.nick_name ownView.isHidden = !isOwner statusDotView.backgroundColor = UIColor(hexStr: model.is_online ? "#67EA76" : "#D8D8D8") @@ -42,6 +42,7 @@ class GroupMemberView2: UIView { } else { setupUnknownDeviceInfo(battery: model.battery) } + refreshMemberReports(for: model) } private func updateDistance(for model: GroupMemberModel, isCurrentUser: Bool) { @@ -214,7 +215,27 @@ class GroupMemberView2: UIView { ] private func setupRx() { - + NotificationCenter.default.addObserver( + self, + selector: #selector(handleUnlockCountDidChange), + name: .unlockCountDidChange, + object: nil + ) + } + + @objc private func handleUnlockCountDidChange() { + guard let model = currentMemberModel else { return } + refreshMemberReports(for: model) + } + + private func refreshMemberReports(for model: GroupMemberModel) { + currentMemberModel = model + let isCurrentUser = model.user_id == AppContextManager.shared.userId + if isCurrentUser { + phoneReportView.configure(with: .currentUser()) + } else { + phoneReportView.configure(with: nil) + } } private func setupUI() { @@ -288,7 +309,7 @@ class GroupMemberView2: UIView { lazy var scrollView: UIScrollView = { let view = UIScrollView() view.backgroundColor = .clear - view.showsHorizontalScrollIndicator = false + view.showsVerticalScrollIndicator = false view.bounces = false view.addSubview(scrollContentView) @@ -300,8 +321,14 @@ class GroupMemberView2: UIView { scrollContentView.addSubview(todayTrackView) todayTrackView.layoutChain - .topToBottomOfView(memberView, offset: 16) - .edges(excludingEdge: .top) + .topToBottomOfView(memberView, offset: 15) + .edgesHorzontal() + + scrollContentView.addSubview(phoneReportView) + phoneReportView.layoutChain + .topToBottomOfView(todayTrackView, offset: 15) + .edgesHorzontal() + .bottom(49 + kSafeBottomMargin) return view }() @@ -728,6 +755,15 @@ class GroupMemberView2: UIView { return label }() + // MARK: - 手机报告 / APP使用记录 + lazy var phoneReportView = MemberPhoneReportView() + /// 保留 UI 实现,当前不挂载到 scroll 内容 + lazy var appUsageView: MemberAppUsageView = { + let view = MemberAppUsageView() + view.isHidden = true + return view + }() + // MARK: - 今日轨迹 lazy var todayTrackView: UIView = { let view = UIView() @@ -942,6 +978,10 @@ class GroupMemberView2: UIView { fatalError("init(coder:) has not been implemented") } + deinit { + NotificationCenter.default.removeObserver(self) + } + } /// 轻量网络状态监听,供面板读取当前路径 diff --git a/QuickLocation/Section/Home/HomeViewController.swift b/QuickLocation/Section/Home/HomeViewController.swift index a846c3af..7cc3438e 100644 --- a/QuickLocation/Section/Home/HomeViewController.swift +++ b/QuickLocation/Section/Home/HomeViewController.swift @@ -88,6 +88,7 @@ class HomeViewController: BaseViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + UnlockCountManager.shared.refreshAuthorizationAndMonitoring() let manager = AuthorizeManager.manager(type: .locationAlways) rootView.warningView.isHidden = manager?.authorizeStatus() == .authorized diff --git a/QuickLocation/Section/Home/MemberPhoneReportView.swift b/QuickLocation/Section/Home/MemberPhoneReportView.swift new file mode 100644 index 00000000..77a8f6a5 --- /dev/null +++ b/QuickLocation/Section/Home/MemberPhoneReportView.swift @@ -0,0 +1,316 @@ +// +// MemberPhoneReportView.swift +// QuickLocation +// + +import UIKit + +struct MemberPhoneReportPreview { + let screenTimeSeconds: Int? + let usageCount: Int + let unlockCount: Int + + static func currentUser(from manager: UnlockCountManager = .shared) -> MemberPhoneReportPreview { + let seconds = manager.todayScreenTimeSeconds + return MemberPhoneReportPreview( + screenTimeSeconds: seconds > 0 ? seconds : nil, + usageCount: manager.todayAppUsageCount, + unlockCount: manager.todayCount + ) + } +} + +/// 成员面板「手机报告」:屏幕使用时长 + 使用次数 + 今日解锁 +final class MemberPhoneReportView: UIView { + + private enum Layout { + static let designCardWidth: CGFloat = 345 + static let leftPanelWidthRatio: CGFloat = 164 / designCardWidth + static let leftPanelAspect: CGFloat = 147 / 164 + static let panelGap: CGFloat = 13 + static let cardInset: CGFloat = 12 + static let usageCardAspect: CGFloat = 69 / 146 + static let usageUnlockGap: CGFloat = 9 + } + + private let titleColor = UIColor(hexStr: "#293445") + private let usageTint = UIColor(hexStr: "#E8FBE8") + private let unlockTint = UIColor(hexStr: "#E8F4FF") + + private let sectionIcon = UIImageView() + private let sectionTitleLab = UILabel() + private let cardView = UIView() + private let leftPanel = UIView() + private let rightPanel = UIView() + private let screenTimeIcon = UIImageView() + private let screenTimeTitleLab = UILabel() + private let screenTimeBadge = ScreenTimeBadgeView() + private let usageCard = UIView() + private let usageIcon = UIImageView() + private let usageTitleLab = UILabel() + private let usageValueLab = UILabel() + private let unlockCard = UIView() + private let unlockIcon = UIImageView() + private let unlockTitleLab = UILabel() + private let unlockValueLab = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + configure(with: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(with preview: MemberPhoneReportPreview?) { + if let preview { + screenTimeBadge.configure(seconds: preview.screenTimeSeconds) + usageValueLab.text = "\(preview.usageCount)" + unlockValueLab.text = "\(preview.unlockCount)" + } else { + screenTimeBadge.configure(seconds: nil) + usageValueLab.text = "**" + unlockValueLab.text = "**" + } + } + + private func setupUI() { + backgroundColor = .clear + + sectionIcon.image = UIImage(named: "Home/member_phone_report") + sectionIcon.contentMode = .scaleAspectFit + addSubview(sectionIcon) + sectionIcon.layoutChain + .top() + .left(15) + .width(18) + .heightToWidth(1) + + sectionTitleLab.text = "手机报告" + sectionTitleLab.font = .systemFont(ofSize: 16, weight: .bold) + sectionTitleLab.textColor = titleColor + addSubview(sectionTitleLab) + sectionTitleLab.layoutChain + .leftToRightOfView(sectionIcon, offset: 6) + .centerY(sectionIcon) + + cardView.backgroundColor = .white + cardView.cornerRadius = 16 + addSubview(cardView) + cardView.layoutChain + .topToBottomOfView(sectionIcon, offset: 12) + .edgesHorzontal(15) + .bottom() + + cardView.addSubview(leftPanel) + cardView.addSubview(rightPanel) + leftPanel.backgroundColor = UIColor(hexStr: "#FFFCEA") + leftPanel.cornerRadius = 14 + rightPanel.backgroundColor = .clear + leftPanel.layoutChain + .top(Layout.cardInset) + .left(Layout.cardInset) + .widthToView(cardView, multiplier: Layout.leftPanelWidthRatio) + .heightToWidth(Layout.leftPanelAspect) + + rightPanel.layoutChain + .top(Layout.cardInset) + .right(Layout.cardInset) + .leftToRightOfView(leftPanel, offset: Layout.panelGap) + .heightToView(leftPanel) + .bottom(Layout.cardInset) + + screenTimeIcon.image = UIImage(named: "Home/member_report_clock") + screenTimeIcon.contentMode = .scaleAspectFit + leftPanel.addSubview(screenTimeIcon) + screenTimeIcon.layoutChain + .top(7) + .left(7) + .width(20) + .height(20) + + screenTimeTitleLab.text = "屏幕使用时长" + screenTimeTitleLab.font = .systemFont(ofSize: 12, weight: .bold) + screenTimeTitleLab.textColor = UIColor(hexStr: "#293445") + leftPanel.addSubview(screenTimeTitleLab) + screenTimeTitleLab.layoutChain + .leftToRightOfView(screenTimeIcon, offset: 6) + .centerY(screenTimeIcon) + + leftPanel.addSubview(screenTimeBadge) + screenTimeBadge.layoutChain + .topToBottomOfView(screenTimeTitleLab, offset: 13) + .centerX(leftPanel, offset: 7) + + let screenTimeBadgeArrow = UIImageView(image: UIImage(named: "Home/member_screenTime_arrow")) + leftPanel.addSubview(screenTimeBadgeArrow) + screenTimeBadgeArrow.layoutChain + .topToBottomOfView(screenTimeBadge) + .centerX(screenTimeBadge) + + let barchartIcon = UIImageView(image: UIImage(named: "Home/member_screenTime")) + leftPanel.addSubview(barchartIcon) + barchartIcon.layoutChain + .centerX() + .topToBottomOfView(screenTimeBadge, offset: 12) + + usageCard.backgroundColor = UIColor(hexStr: "#F8FFEE") + usageCard.cornerRadius = 14 + rightPanel.addSubview(usageCard) + usageCard.layoutChain + .top() + .edgesHorzontal() + .heightToWidth(Layout.usageCardAspect) + + usageIcon.image = UIImage(named: "Home/member_usage_count") + usageIcon.contentMode = .scaleAspectFit + usageCard.addSubview(usageIcon) + usageIcon.layoutChain + .top(8) + .left(10) + .width(20) + .height(20) + + usageTitleLab.text = "使用次数" + usageTitleLab.font = .systemFont(ofSize: 12, weight: .bold) + usageTitleLab.textColor = UIColor(hexStr: "#293445") + usageCard.addSubview(usageTitleLab) + usageTitleLab.layoutChain + .leftToRightOfView(usageIcon, offset: 6) + .centerY(usageIcon) + .right(relation: .lessThanOrEqual) + + usageValueLab.font = .systemFont(ofSize: 24, weight: .bold) + usageValueLab.textColor = UIColor(hexStr: "#83DE00") + usageCard.addSubview(usageValueLab) + usageValueLab.layoutChain + .topToBottomOfView(usageTitleLab, offset: 4) + .centerX() + + let usageValueUnitLab = UILabel() + usageValueUnitLab.text = "次" + usageValueUnitLab.textColor = UIColor(hexStr: "#293445") + usageValueUnitLab.font = .systemFont(ofSize: 12, weight: .bold) + usageCard.addSubview(usageValueUnitLab) + usageValueUnitLab.layoutChain + .leftToRightOfView(usageValueLab, offset: 3) + .bottomToView(usageValueLab, offset: -4) + + unlockCard.backgroundColor = UIColor(hexStr: "#F2FEFF") + unlockCard.cornerRadius = 14 + rightPanel.addSubview(unlockCard) + unlockCard.layoutChain + .topToBottomOfView(usageCard, offset: Layout.usageUnlockGap) + .edgesHorzontal() + .heightToWidth(Layout.usageCardAspect) + .bottom() + + unlockIcon.image = UIImage(named: "Home/member_daily_unlock") + unlockIcon.contentMode = .scaleAspectFit + unlockCard.addSubview(unlockIcon) + unlockIcon.layoutChain + .top(8) + .left(10) + .width(20) + .height(20) + + unlockTitleLab.text = "今日解锁" + unlockTitleLab.font = .systemFont(ofSize: 12, weight: .bold) + unlockTitleLab.textColor = UIColor(hexStr: "#293445") + unlockCard.addSubview(unlockTitleLab) + unlockTitleLab.layoutChain + .leftToRightOfView(unlockIcon, offset: 6) + .centerY(unlockIcon) + .right(relation: .lessThanOrEqual) + + unlockValueLab.font = .systemFont(ofSize: 24, weight: .bold) + unlockValueLab.textColor = UIColor(hexStr: "#08B6C4") + unlockCard.addSubview(unlockValueLab) + unlockValueLab.layoutChain + .topToBottomOfView(unlockTitleLab, offset: 4) + .centerX() + + let unlockValueUnitLab = UILabel() + unlockValueUnitLab.text = "次" + unlockValueUnitLab.textColor = UIColor(hexStr: "#293445") + unlockValueUnitLab.font = .systemFont(ofSize: 12, weight: .bold) + unlockCard.addSubview(unlockValueUnitLab) + unlockValueUnitLab.layoutChain + .leftToRightOfView(unlockValueLab, offset: 3) + .bottomToView(unlockValueLab, offset: -4) + } +} + +// MARK: - 屏幕时长 Badge + +private final class ScreenTimeBadgeView: UIView { + + private let contentStack = UIStackView() + private let numberColor = UIColor(hexStr: "#FF6B4A") + private let unitColor = UIColor(hexStr: "#293445") + private let numberFont = UIFont.systemFont(ofSize: 24, weight: .heavy) + private let unitFont = UIFont.systemFont(ofSize: 14, weight: .heavy) + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .white + cornerRadius = 11 + contentStack.axis = .horizontal + contentStack.alignment = .lastBaseline + contentStack.spacing = 2 + addSubview(contentStack) + contentStack.layoutChain + .edges(UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(seconds: Int?) { + contentStack.arrangedSubviews.forEach { view in + contentStack.removeArrangedSubview(view) + view.removeFromSuperview() + } + + guard let seconds, seconds > 0 else { + contentStack.addArrangedSubview(makeLabel(text: "**", font: numberFont, color: numberColor)) + return + } + + let hours = seconds / 3600 + let minutes = (seconds % 3600) / 60 + + if hours > 0 { + appendPart(number: "\(hours)", unit: "h") + if minutes > 0 { + contentStack.addArrangedSubview(makeSpacer(width: 4)) + appendPart(number: "\(minutes)", unit: "m") + } + } else { + appendPart(number: "\(minutes)", unit: "m") + } + } + + private func appendPart(number: String, unit: String) { + contentStack.addArrangedSubview(makeLabel(text: number, font: numberFont, color: numberColor)) + contentStack.addArrangedSubview(makeLabel(text: unit, font: unitFont, color: unitColor)) + } + + private func makeLabel(text: String, font: UIFont, color: UIColor) -> UILabel { + let label = UILabel() + label.text = text + label.font = font + label.textColor = color + return label + } + + private func makeSpacer(width: CGFloat) -> UIView { + let spacer = UIView() + spacer.translatesAutoresizingMaskIntoConstraints = false + spacer.widthAnchor.constraint(equalToConstant: width).isActive = true + return spacer + } +} diff --git a/QuickLocation/Section/Mine/Account/AccountView.swift b/QuickLocation/Section/Mine/Account/AccountView.swift index f87a487a..53ef00a4 100644 --- a/QuickLocation/Section/Mine/Account/AccountView.swift +++ b/QuickLocation/Section/Mine/Account/AccountView.swift @@ -14,6 +14,13 @@ class AccountView: UIView { var disposeBag = DisposeBag() private func setupRx() { + copyBtn.rx.tap + .subscribe(onNext: { _ in + UIPasteboard.general.string = AppContextManager.shared.userId + DLToast.showSuccess(text: "已复制") + }) + .disposed(by: disposeBag) + phoneView.rx.tapGesture.subscribe(onNext: { _ in AppRouter.push(Route.changePhone) }).disposed(by: disposeBag) @@ -116,13 +123,20 @@ class AccountView: UIView { .left(15) .centerY() + view.addSubview(copyBtn) + copyBtn.layoutChain + .right(15) + .centerY() + .width(14) + .height(14) + let contentLab = UILabel() contentLab.text = AppContextManager.shared.userId contentLab.font = .systemFont(ofSize: 14, weight: .medium) contentLab.textColor = UIColor(hexStr: "#1A1A1A") view.addSubview(contentLab) contentLab.layoutChain - .right(15) + .rightToLeftOfView(copyBtn, offset: -2) .centerY() let line = UIView() @@ -136,6 +150,12 @@ class AccountView: UIView { return view }() + lazy var copyBtn: UIButton = { + let btn = UIButton(type: .custom) + btn.setImage(UIImage(named: "Mine/copy"), for: .normal) + return btn + }() + /// 手机号 lazy var phoneView: UIView = { let view = UIView() diff --git a/QuickLocation/Section/Mine/MinePhotoWallView.swift b/QuickLocation/Section/Mine/MinePhotoWallView.swift new file mode 100644 index 00000000..17829f07 --- /dev/null +++ b/QuickLocation/Section/Mine/MinePhotoWallView.swift @@ -0,0 +1,770 @@ +// +// MinePhotoWallView.swift +// QuickLocation +// + +import UIKit +import RxSwift +import RxCocoa + +/// 「我的」页顶部:绳线 + 成员照片墙 + 信息区 +final class MinePhotoWallView: UIView { + + var onCenteredMember: ((GroupMemberModel) -> Void)? + var onCopyID: ((String) -> Void)? + var onTapSelfAvatar: (() -> Void)? + + private var members: [GroupMemberModel] = [] + /// 与 members 对齐的夹子颜色下标(相邻不同色) + private var clipColorIndices: [Int] = [] + private var ownerUserId: String = "" + private var centeredIndex: Int = 0 + private var isDragging = false + private var lastLayoutWidth: CGFloat = 0 + + /// cell 包络按选中相框 70×90 + 夹子留白 + private let itemSize = CGSize(width: 72, height: 110) + private let itemSpacing: CGFloat = 18 + /// 左右边缘淡化带宽 + private let edgeFadeWidth: CGFloat = 56 + /// 绳两端距照片墙顶 + private let ropeEndY: CGFloat = 30 + /// 绳中间下垂量 + private let ropeSag: CGFloat = 28 + private var clipAttachInCell: CGFloat { MinePolaroidCell.clipAttachOffset } + + private let selectedSize = MinePolaroidCell.selectedFrameSize + private let unselectedSize = MinePolaroidCell.unselectedFrameSize + + /// 绳参数 t∈[-1,1] + private func ropeT(atX x: CGFloat) -> CGFloat { + let w = max(bounds.width, 1) + let t = (x - w * 0.5) / (w * 0.5) + return max(-1, min(1, t)) + } + + /// 绳在 x 处的 y:两端高、中间低 + private func ropeY(atX x: CGFloat) -> CGFloat { + let t = ropeT(atX: x) + return ropeEndY + ropeSag * (1 - t * t) + } + + /// 绳切线角(UIKit 正角顺时针),使照片跟随弧度倾斜 + private func ropeAngle(atX x: CGFloat) -> CGFloat { + let w = max(bounds.width, 1) + let t = ropeT(atX: x) + // y = end + sag*(1-t²), t=(x-w/2)/(w/2) → dy/dx = -4*sag*t/w + return atan(-4 * ropeSag * t / w) + } + + private var ropeCenterY: CGFloat { ropeEndY + ropeSag } + + // MARK: - Public + + func reload(members: [GroupMemberModel], ownerUserId: String, preferUserId: String) { + self.members = members + self.ownerUserId = ownerUserId + self.clipColorIndices = Self.makeClipColorIndices(for: members) + collectionView.reloadData() + + guard !members.isEmpty else { + updateInfo(nil) + return + } + + let idx: Int + if let i = members.firstIndex(where: { $0.user_id == preferUserId }) { + idx = i + } else if let i = members.firstIndex(where: { $0.user_id == AppContextManager.shared.userId }) { + idx = i + } else { + idx = 0 + } + centeredIndex = idx + collectionView.layoutIfNeeded() + scrollToIndex(idx, animated: false) + collectionView.layoutIfNeeded() + applyArcAndFocus() + // contentOffset / cell 布局下一拍再贴一次,避免首屏右侧离绳 + DispatchQueue.main.async { [weak self] in + self?.applyArcAndFocus() + } + updateInfo(members[idx]) + onCenteredMember?(members[idx]) + } + + /// 相邻夹子不同色;同成员尽量稳定 + private static func makeClipColorIndices(for members: [GroupMemberModel]) -> [Int] { + let colorCount = MinePolaroidCell.clipAssetNames.count + guard colorCount > 1 else { return Array(repeating: 0, count: members.count) } + var result: [Int] = [] + var prev = -1 + for (i, m) in members.enumerated() { + var options = Array(0..2) + if i == members.count - 1, members.count > 2, let first = result.first { + options = options.filter { $0 != first } + if options.isEmpty { options = Array(0.. 0.5 + if widthChanged, let layout = collectionView.collectionViewLayout as? MinePhotoWallFlowLayout { + lastLayoutWidth = bounds.width + let side = (bounds.width - itemSize.width) / 2 + let topInset = ropeCenterY - clipAttachInCell + layout.sectionInset = UIEdgeInsets(top: max(0, topInset), left: side, bottom: 0, right: side) + layout.invalidateLayout() + collectionView.layoutIfNeeded() + } + applyArcAndFocus() + } + + private func updateRopePath() { + let w = bounds.width + guard w > 0 else { return } + let path = UIBezierPath() + path.move(to: CGPoint(x: 0, y: ropeEndY)) + path.addQuadCurve( + to: CGPoint(x: w, y: ropeEndY), + controlPoint: CGPoint(x: w * 0.5, y: ropeEndY + ropeSag * 2) + ) + ropeLayer.path = path.cgPath + ropeLayer.frame = bounds + + let shadowPath = UIBezierPath() + let dy: CGFloat = 1.5 + shadowPath.move(to: CGPoint(x: 0, y: ropeEndY + dy)) + shadowPath.addQuadCurve( + to: CGPoint(x: w, y: ropeEndY + dy), + controlPoint: CGPoint(x: w * 0.5, y: ropeEndY + ropeSag * 2 + dy) + ) + ropeShadowLayer.path = shadowPath.cgPath + ropeShadowLayer.frame = bounds + } + + private func scrollToIndex(_ index: Int, animated: Bool) { + guard index >= 0, index < members.count else { return } + let indexPath = IndexPath(item: index, section: 0) + collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: animated) + } + + private func nearestCenterIndex() -> Int { + let centerX = collectionView.contentOffset.x + collectionView.bounds.width / 2 + var best = 0 + var bestDist = CGFloat.greatestFiniteMagnitude + for i in 0.. 0.5 + if !moved && !self.isDragging { + self.playSwayOnce() + } + } + } + + /// 衣夹贴绳 + 切线倾斜 + 选中 70×90 / 未选中 60×70 + 左右边缘淡化 + private func applyArcAndFocus() { + guard bounds.width > 0 else { return } + let scrollCenterX = collectionView.contentOffset.x + collectionView.bounds.width / 2 + let span = itemSize.width + itemSpacing + for cell in collectionView.visibleCells { + guard let polaroid = cell as? MinePolaroidCell, + let indexPath = collectionView.indexPath(for: cell), + let attrs = collectionView.layoutAttributesForItem(at: indexPath) else { continue } + + cell.transform = .identity + + let dist = abs(attrs.center.x - scrollCenterX) + let t = min(dist / span, 1) + // 相框真实尺寸插值(不含夹子),避免非等比 scale + 旋转剪切变形 + let fw = selectedSize.width + (unselectedSize.width - selectedSize.width) * t + let fh = selectedSize.height + (unselectedSize.height - selectedSize.height) * t + + let frameInSelf = collectionView.convert(attrs.frame, to: self) + let attachX = frameInSelf.midX + let attachY = frameInSelf.minY + MinePolaroidCell.clipAttachOffset + let ty = ropeY(atX: attachX) - attachY + 2 + let angle = ropeAngle(atX: attachX) + + // 左右屏边淡化到 70% 透明度,仅相框淡化,夹子保持不透明 + let edgeT = min( + 1, + max(0, frameInSelf.midX / edgeFadeWidth), + max(0, (bounds.width - frameInSelf.midX) / edgeFadeWidth) + ) + let frameAlpha = 0.7 + 0.3 * edgeT + polaroid.setFrameAlpha(frameAlpha) + + polaroid.applyFocus(frameSize: CGSize(width: fw, height: fh), angle: angle) + polaroid.setSelectedLook(dist < itemSize.width * 0.45) + cell.transform = CGAffineTransform(translationX: 0, y: ty) + } + } + + private func updateInfo(_ model: GroupMemberModel?) { + guard let model = model else { + titleLab.text = " " + titleLab.isHidden = false + daysRow.isHidden = true + idLab.text = " " + return + } + let isSelf = model.user_id == AppContextManager.shared.userId + if isSelf { + titleLab.isHidden = false + daysRow.isHidden = true + titleLab.text = model.nick_name + } else { + titleLab.isHidden = true + daysRow.isHidden = false + daysBadge.text = model.joinDaysDisplay + } + idLab.text = "ID:\(model.user_id)" + } + + private func playSwayOnce() { + for cell in collectionView.visibleCells { + (cell as? MinePolaroidCell)?.playSwayOnce() + } + } +} + +// MARK: - Collection + +extension MinePhotoWallView: UICollectionViewDataSource, UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + members.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell: MinePolaroidCell = collectionView.dequeueReusableCell(for: indexPath) + let model = members[indexPath.item] + let colorIdx = indexPath.item < clipColorIndices.count ? clipColorIndices[indexPath.item] : 0 + cell.configure( + model: model, + isOwner: !ownerUserId.isEmpty && ownerUserId == model.user_id, + clipColorIndex: colorIdx + ) + return cell + } + + func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + // 新露出的 cell 立刻贴绳倾斜 + DispatchQueue.main.async { [weak self] in + self?.applyArcAndFocus() + } + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let model = members[indexPath.item] + if model.user_id == AppContextManager.shared.userId { + onTapSelfAvatar?() + } + centeredIndex = indexPath.item + let before = collectionView.contentOffset + scrollToIndex(indexPath.item, animated: true) + applyArcAndFocus() + updateInfo(model) + onCenteredMember?(model) + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + if abs(self.collectionView.contentOffset.x - before.x) < 0.5, !self.isDragging { + self.playSwayOnce() + } + } + } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + isDragging = true + for cell in collectionView.visibleCells { + (cell as? MinePolaroidCell)?.stopSway() + } + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + applyArcAndFocus() + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !decelerate { + isDragging = false + settleToNearest() + } + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + isDragging = false + settleToNearest() + } + + func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { + applyArcAndFocus() + if !isDragging { + playSwayOnce() + } + } +} + +// MARK: - Flow layout(吸附居中) + +private final class MinePhotoWallFlowLayout: UICollectionViewFlowLayout { + override func targetContentOffset( + forProposedContentOffset proposedContentOffset: CGPoint, + withScrollingVelocity velocity: CGPoint + ) -> CGPoint { + guard let cv = collectionView else { return proposedContentOffset } + let rect = CGRect(origin: proposedContentOffset, size: cv.bounds.size) + guard let attrs = layoutAttributesForElements(in: rect), !attrs.isEmpty else { + return proposedContentOffset + } + let centerX = proposedContentOffset.x + cv.bounds.width / 2 + var best = attrs[0] + for a in attrs where abs(a.center.x - centerX) < abs(best.center.x - centerX) { + best = a + } + return CGPoint(x: best.center.x - cv.bounds.width / 2, y: proposedContentOffset.y) + } +} + +// MARK: - Polaroid cell + +final class MinePolaroidCell: UICollectionViewCell { + + static let selectedFrameSize = CGSize(width: 70, height: 90) + static let unselectedFrameSize = CGSize(width: 60, height: 70) + /// 夹子 16×24,咬合点偏上,整体略挂在绳下 + static let clipSize = CGSize(width: 16, height: 24) + static let clipAttachOffset: CGFloat = clipSize.height * 0.32 + private static let frameTopInset: CGFloat = 12 + private static let avatarInset: CGFloat = 5 + + static let clipAssetNames = [ + "Mine/clothespin", + "Mine/clothespin_blue", + "Mine/clothespin_green", + "Mine/clothespin_pink" + ] + + private static let swayKey = "minePolaroidSway" + private var isOwner = false + private var focusFrameSize = selectedFrameSize + private var focusAngle: CGFloat = 0 + + func configure(model: GroupMemberModel, isOwner: Bool, clipColorIndex: Int) { + self.isOwner = isOwner + avatarImg.image = model.userIcon + ownerTag.isHidden = !isOwner + let idx = ((clipColorIndex % Self.clipAssetNames.count) + Self.clipAssetNames.count) % Self.clipAssetNames.count + clipImg.image = UIImage(named: Self.clipAssetNames[idx]) + stopSway() + } + + /// 按真实相框尺寸布局,整块 polaroidHost 绕衣夹只做旋转(无非等比 scale) + func applyFocus(frameSize: CGSize, angle: CGFloat) { + focusFrameSize = frameSize + focusAngle = angle + layoutPolaroidHostIfNeeded() + polaroidHost.transform = CGAffineTransform(rotationAngle: angle) + } + + func setFrameAlpha(_ alpha: CGFloat) { + frameView.alpha = alpha + clipImg.alpha = 1 + } + + func setSelectedLook(_ selected: Bool) { + heartImg.isHidden = !selected + if selected { + ownerTag.isHidden = true + } else { + ownerTag.isHidden = !isOwner + } + } + + func playSwayOnce() { + stopSway() + let anim = CAKeyframeAnimation(keyPath: "transform.rotation.z") + anim.values = [0, 0.1, 0, -0.1, 0] + anim.keyTimes = [0, 0.25, 0.5, 0.75, 1] + anim.duration = 1.6 + anim.repeatCount = 1 + anim.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + anim.isRemovedOnCompletion = true + anim.isAdditive = true + swayHost.layer.add(anim, forKey: Self.swayKey) + } + + func stopSway() { + swayHost.layer.removeAnimation(forKey: Self.swayKey) + } + + override func prepareForReuse() { + super.prepareForReuse() + stopSway() + heartImg.isHidden = true + ownerTag.isHidden = true + isOwner = false + focusFrameSize = Self.selectedFrameSize + focusAngle = 0 + polaroidHost.transform = .identity + transform = .identity + frameView.alpha = 1 + clipImg.alpha = 1 + } + + override func layoutSubviews() { + super.layoutSubviews() + layoutPolaroidHostIfNeeded() + polaroidHost.transform = CGAffineTransform(rotationAngle: focusAngle) + } + + /// 用 frame 布局 host,锚点钉在衣夹;相框用当前 focusFrameSize + private func layoutPolaroidHostIfNeeded() { + let fw = focusFrameSize.width + let fh = focusFrameSize.height + let hostW = max(fw, Self.clipSize.width) + let hostH = Self.frameTopInset + fh + polaroidHost.bounds = CGRect(x: 0, y: 0, width: hostW, height: hostH) + + let anchor = CGPoint(x: 0.5, y: Self.clipAttachOffset / max(hostH, 1)) + polaroidHost.layer.anchorPoint = anchor + polaroidHost.layer.position = CGPoint( + x: contentView.bounds.midX, + y: Self.clipAttachOffset + ) + + swayHost.frame = polaroidHost.bounds + clipImg.frame = CGRect( + x: (hostW - Self.clipSize.width) / 2, + y: 0, + width: Self.clipSize.width, + height: Self.clipSize.height + ) + frameView.frame = CGRect(x: (hostW - fw) / 2, y: Self.frameTopInset, width: fw, height: fh) + frameView.cornerRadius = 18 * (fw / Self.selectedFrameSize.width) + + let avatarSide = max(0, fw - Self.avatarInset * 2) + avatarImg.frame = CGRect( + x: Self.avatarInset, + y: Self.avatarInset, + width: avatarSide, + height: avatarSide + ) + avatarImg.cornerRadius = max(4, avatarSide * 0.2) + + let captionMidY = Self.avatarInset + avatarSide + (fh - Self.avatarInset - avatarSide) / 2 + heartImg.bounds = CGRect(x: 0, y: 0, width: 12, height: 10) + heartImg.center = CGPoint(x: fw / 2, y: captionMidY) + ownerTag.sizeToFit() + ownerTag.center = CGPoint(x: fw / 2, y: captionMidY) + } + + override init(frame: CGRect) { + super.init(frame: frame) + contentView.clipsToBounds = false + clipsToBounds = false + contentView.addSubview(polaroidHost) + polaroidHost.addSubview(swayHost) + swayHost.addSubview(frameView) + frameView.addSubview(avatarImg) + frameView.addSubview(heartImg) + frameView.addSubview(ownerTag) + swayHost.addSubview(clipImg) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private lazy var polaroidHost: UIView = { + let v = UIView() + v.backgroundColor = .clear + return v + }() + + private lazy var swayHost: UIView = { + let v = UIView() + v.backgroundColor = .clear + return v + }() + + private lazy var frameView: UIView = { + let v = UIView() + v.backgroundColor = .white + v.cornerRadius = 18 + v.layer.shadowColor = UIColor.black.withAlphaComponent(0.12).cgColor + v.layer.shadowOffset = CGSize(width: 0, height: 2) + v.layer.shadowRadius = 4 + v.layer.shadowOpacity = 1 + return v + }() + + private lazy var avatarImg: UIImageView = { + let iv = UIImageView() + iv.contentMode = .scaleAspectFill + iv.clipsToBounds = true + iv.cornerRadius = 12 + iv.backgroundColor = UIColor(hexStr: "#E8EEF2") + return iv + }() + + private lazy var clipImg: UIImageView = { + let iv = UIImageView(image: UIImage(named: "Mine/clothespin")) + iv.contentMode = .scaleAspectFit + return iv + }() + + private lazy var heartImg: UIImageView = { + let iv = UIImageView() + if #available(iOS 13.0, *) { + iv.image = UIImage(systemName: "heart.fill") + iv.tintColor = UIColor(hexStr: "#FF6B9D") + } + iv.contentMode = .scaleAspectFit + iv.isHidden = true + return iv + }() + + private lazy var ownerTag: UILabel = { + let label = PaddingLabel() + label.text = "圈主" + label.font = .systemFont(ofSize: 9, weight: .semibold) + label.textColor = .white + label.backgroundColor = UIColor(hexStr: "#16B3FF") + label.cornerRadius = 4 + label.clipsToBounds = true + label.textAlignment = .center + label.insets = UIEdgeInsets(top: 1, left: 5, bottom: 1, right: 5) + label.isHidden = true + return label + }() +} + +private final class PaddingLabel: UILabel { + var insets = UIEdgeInsets.zero + override func drawText(in rect: CGRect) { + super.drawText(in: rect.inset(by: insets)) + } + override var intrinsicContentSize: CGSize { + let size = super.intrinsicContentSize + return CGSize(width: size.width + insets.left + insets.right, + height: size.height + insets.top + insets.bottom) + } +} + +extension GroupMemberModel { + var joinDaysDisplay: String { + if join_days > 0 { return "\(join_days)" } + if join_time > 0 { + let days = max(1, Int((Date().timeIntervalSince1970 - TimeInterval(join_time) / 1000) / 86400)) + return "\(days)" + } + return "--" + } +} diff --git a/QuickLocation/Section/Mine/MineView.swift b/QuickLocation/Section/Mine/MineView.swift index d91282c7..69110516 100644 --- a/QuickLocation/Section/Mine/MineView.swift +++ b/QuickLocation/Section/Mine/MineView.swift @@ -2,7 +2,7 @@ // MineView.swift // QuickLocation // -// Created based on Lanhu design: 我的·版本6 +// Created based on Lanhu design: 我的·版本6 / 照片墙改版 // import UIKit @@ -13,20 +13,11 @@ final class MineView: UIView { var disposeBag = DisposeBag() - // MARK: - Design Constants (iOS points, from Lanhu 版本6 @2x) - private let headerHeight: CGFloat = 160 - private let avatarSize: CGFloat = 64 - private let editBtnSize: CGFloat = 30 - private let copyIconSize: CGFloat = 14 - private let vipCardHeight: CGFloat = 90 - private let settingsRowHeight: CGFloat = 52 + private let quickItemTitles = ["权限检测", "数据安全", "状态设置", "隐私设置"] - // MARK: - Colors - private let titleColor = UIColor(hexStr: "#1A1A1A") - private let subtitleColor = UIColor(hexStr: "#767676") - private let vipBtnTextColor = UIColor(hexStr: "#00343B") + var onQuickTap: ((String) -> Void)? func setupData(_ model: UserConfigModel) { guard AppContextManager.shared.systemConfig?.isIntercept == false else { @@ -47,70 +38,33 @@ final class MineView: UIView { vipActivateButton.setTitle("点击续费", for: .normal) vipActivateButton.isHidden = model.vip == 3 } - - private func setupRx() { - avatarImageView.rx.tapGesture.subscribe { _ in - AppRouter.push(Route.myProfile) - }.disposed(by: disposeBag) - } - - // MARK: - UI Components - private lazy var headerBgImageView: UIImageView = { - let iv = UIImageView() - iv.image = UIImage(named: "Mine/header_bg") - iv.contentMode = .scaleAspectFill - iv.clipsToBounds = true - return iv + + // MARK: - Components + + private lazy var scrollView: UIScrollView = { + let sv = UIScrollView() + sv.backgroundColor = UIColor(hexStr: "#FAFAFA") + sv.showsVerticalScrollIndicator = false +// sv.alwaysBounceVertical = true + return sv }() - lazy var avatarImageView: UIImageView = { - let iv = UIImageView() - iv.isUserInteractionEnabled = true - iv.contentMode = .scaleAspectFill - iv.layer.cornerRadius = avatarSize / 2 - iv.layer.borderWidth = 2 - iv.layer.borderColor = UIColor.white.cgColor - iv.clipsToBounds = true - iv.layer.shadowColor = UIColor.black.withAlphaComponent(0.2).cgColor - iv.layer.shadowOffset = .zero - iv.layer.shadowRadius = 2 - iv.layer.shadowOpacity = 1 - iv.backgroundColor = UIColor(hexStr: "#E0E0E0") - iv.image = UIImage(named: "Mine/default_avatar") - return iv + private lazy var contentView: UIView = { + let v = UIView() + v.backgroundColor = .clear + return v }() - lazy var nameLabel: UILabel = { - let label = UILabel() - label.font = .systemFont(ofSize: 16, weight: .heavy) - label.textColor = titleColor - label.text = " " - return label + private lazy var headerBgView: UIView = { + let v = UIView() + v.backgroundColor = .clear + return v }() - lazy var editButton: UIButton = { - let btn = UIButton(type: .custom) - btn.setImage(UIImage(named: "Mine/edit"), for: .normal) - btn.backgroundColor = .clear - return btn + lazy var photoWall: MinePhotoWallView = { + MinePhotoWallView() }() - lazy var idLabel: UILabel = { - let label = UILabel() - label.font = .systemFont(ofSize: 14, weight: .medium) - label.textColor = subtitleColor - label.text = " " - return label - }() - - lazy var copyButton: UIButton = { - let btn = UIButton(type: .custom) - btn.setImage(UIImage(named: "Mine/copy"), for: .normal) - btn.backgroundColor = .clear - return btn - }() - - // VIP Card lazy var vipCardView: UIView = { let view = UIView() view.layer.cornerRadius = 16 @@ -124,7 +78,7 @@ final class MineView: UIView { iv.contentMode = .scaleToFill return iv }() - + lazy var maskBgView: UIImageView = { let iv = UIImageView() iv.image = UIImage(named: "Mine/mask_bg") @@ -132,15 +86,15 @@ final class MineView: UIView { iv.isHidden = true return iv }() - + lazy var vipTitleLab: UILabel = { let label = UILabel() - label.font = .systemFont(ofSize: 18, weight: .bold) + label.font = FontManager.boboBold(18) label.textColor = .white label.text = " " return label }() - + lazy var vipContentLab: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 12, weight: .medium) @@ -154,27 +108,27 @@ final class MineView: UIView { btn.backgroundColor = UIColor(hexStr: "#C0FF54") btn.setTitle("立即开通", for: .normal) btn.setTitleColor(UIColor(hexStr: "#00343B"), for: .normal) - btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .bold) - btn.cornerRadius = 14 + btn.titleLabel?.font = FontManager.boboBold(14) + btn.cornerRadius = 9 btn.isHidden = true return btn }() - // Profile transition background (100pt section below VIP card) - private lazy var profileBgImageView: UIImageView = { - let iv = UIImageView() - iv.image = UIImage(named: "Mine/profile_bg") - iv.contentMode = .scaleToFill - return iv + private lazy var quickCardView: UIView = { + let v = UIView() + v.backgroundColor = .white + v.cornerRadius = 20 + return v }() - private lazy var bottomFillView: UIView = { - let view = UIView() - view.backgroundColor = UIColor(hexStr: "#F5FBFB") - return view + private lazy var quickStack: UIStackView = { + let stack = UIStackView() + stack.axis = .horizontal + stack.distribution = .fillEqually + stack.alignment = .center + return stack }() - // Settings Card private lazy var settingsCardView: UIView = { let view = UIView() view.backgroundColor = .white @@ -197,164 +151,192 @@ final class MineView: UIView { return tv }() - // MARK: - Callbacks - var onMenuTap: ((Int) -> Void)? - - // MARK: - Dynamic Height private var settingsCardHeightConstraint: NSLayoutConstraint? private var contentSizeObservation: NSKeyValueObservation? - // MARK: - Init + // 兼容旧绑定(已迁到 photoWall) + lazy var avatarImageView: UIImageView = UIImageView() + lazy var nameLabel: UILabel = UILabel() + lazy var idLabel: UILabel = UILabel() + lazy var editButton: UIButton = UIButton(type: .custom) + lazy var copyButton: UIButton = UIButton(type: .custom) + override init(frame: CGRect) { super.init(frame: frame) setupUI() setupLayout() - setupRx() + setupQuickActions() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - // MARK: - Setup private func setupUI() { - backgroundColor = .white + backgroundColor = UIColor(hexStr: "#FAFAFA") - addSubview(headerBgImageView) - addSubview(avatarImageView) - addSubview(nameLabel) - addSubview(editButton) - addSubview(idLabel) - addSubview(copyButton) + addSubview(scrollView) + scrollView.addSubview(contentView) + contentView.addSubview(headerBgView) - addSubview(vipCardView) + // 照片墙固定顶部,不随列表滚动 + addSubview(photoWall) + + contentView.addSubview(vipCardView) vipCardView.addSubview(vipCardBgImageView) vipCardView.addSubview(vipActivateButton) vipCardView.addSubview(vipTitleLab) vipCardView.addSubview(vipContentLab) vipCardView.addSubview(maskBgView) - addSubview(profileBgImageView) - addSubview(bottomFillView) - addSubview(settingsCardView) + contentView.addSubview(quickCardView) + quickCardView.addSubview(quickStack) + + contentView.addSubview(settingsCardView) settingsCardView.addSubview(settingsTableView) } private func setupLayout() { - // Header - headerBgImageView.layoutChain - .top().left().right() - .height(headerHeight) + photoWall.layoutChain + .top(40) + .edgesHorzontal() - // Avatar - avatarImageView.layoutChain - .top(kStatusBarHeight + 44) - .left(32) - .size(CGSize(width: avatarSize, height: avatarSize)) + scrollView.layoutChain + .topToBottomOfView(photoWall) + .edgesHorzontal() + .bottom() - // Name - nameLabel.layoutChain - .leftToRightOfView(avatarImageView, offset: 20) - .topToView(avatarImageView, offset: 8) + contentView.layoutChain + .edges() + .widthToView(scrollView) - // Edit button - editButton.layoutChain - .right(16) - .centerY(nameLabel) - .size(CGSize(width: editBtnSize, height: editBtnSize)) - - // ID - idLabel.layoutChain - .leftToRightOfView(avatarImageView, offset: 20) - .topToBottomOfView(nameLabel, offset: 4) - - // Copy button - copyButton.layoutChain - .leftToRightOfView(idLabel, offset: 4) - .centerY(idLabel) - .size(CGSize(width: copyIconSize, height: copyIconSize)) - - // VIP Card vipCardView.layoutChain - .topToBottomOfView(avatarImageView, offset: 28) + .top(12) .left(16).right(16) .height(vipCardHeight) - vipCardBgImageView.layoutChain - .edges() + vipCardBgImageView.layoutChain.edges() vipTitleLab.layoutChain .top(22) .left(77) - + vipContentLab.layoutChain .topToBottomOfView(vipTitleLab, offset: 6) .leftToView(vipTitleLab) - + vipActivateButton.layoutChain .right(19).centerY() .width(70).height(28) maskBgView.layoutChain.edges() - - // Profile transition background (100pt, overlaps VIP card bottom half) - profileBgImageView.layoutChain - .topToView(vipCardView, offset: 32) - .left().right() - .height(100) - // Fill below profile transition - bottomFillView.layoutChain - .topToBottomOfView(profileBgImageView) - .left().right().bottom() - - // Settings Card (dynamic height, scrolls when exceeds area above tabbar) - settingsCardView.layoutChain + quickCardView.layoutChain .topToBottomOfView(vipCardView, offset: 20) .left(16).right(16) + .height(90) - settingsCardHeightConstraint = settingsCardView.heightAnchor.constraint(equalToConstant: 312) + quickStack.layoutChain + .edges(all: 8) + + settingsCardView.layoutChain + .topToBottomOfView(quickCardView, offset: 16) + .left(16).right(16) + .bottom(20) + + settingsCardHeightConstraint = settingsCardView.heightAnchor.constraint(equalToConstant: 208) settingsCardHeightConstraint?.isActive = true - settingsTableView.layoutChain - .edges() + settingsTableView.layoutChain.edges() contentSizeObservation = settingsTableView.observe(\.contentSize, options: [.new, .initial]) { [weak self] _, _ in self?.refreshTableViewHeight() } } - // MARK: - Layout + private func setupQuickActions() { + let icons = [ + "Mine/quick_permission", + "Mine/quick_data_security", + "Mine/quick_status", + "Mine/quick_privacy" + ] + for (i, title) in quickItemTitles.enumerated() { + let item = makeQuickItem(icon: icons[i], title: title) + item.tag = i + item.rx.tapGesture + .subscribe(onNext: { [weak self] _ in + self?.onQuickTap?(title) + }) + .disposed(by: disposeBag) + quickStack.addArrangedSubview(item) + } + } + + private func makeQuickItem(icon: String, title: String) -> UIView { + let wrap = UIView() + wrap.isUserInteractionEnabled = true + let view = UIView() + view.backgroundColor = UIColor(hexStr: "#F3F5F8") + view.cornerRadius = 16 + + let iv = UIImageView(image: UIImage(named: icon)) + iv.contentMode = .scaleAspectFill + view.addSubview(iv) + + let lab = UILabel() + lab.text = title + lab.font = .systemFont(ofSize: 12, weight: .medium) + lab.textColor = UIColor(hexStr: "#293445") + lab.textAlignment = .center + wrap.addSubview(view) + wrap.addSubview(lab) + view.layoutChain + .top(8) + .centerX() + .width(40) + .height(40) + iv.layoutChain + .centerY() + .centerX() + .width(24) + .height(24) + lab.layoutChain + .topToBottomOfView(view, offset: 6) + .edgesHorzontal(2) + .bottom(4) + return wrap + } + override func layoutSubviews() { super.layoutSubviews() + // 顶部淡色渐变氛围 + setGradientLayer( + frame: CGRect(x: 0, y: 0, width: bounds.width, height: min(280, bounds.height * 0.35)), + startPoint: CGPoint(x: 0.5, y: 0), + endPoint: CGPoint(x: 0.5, y: 1), + colors: [UIColor(hexStr: "#D8F4FF"), UIColor(hexStr: "#FAFAFA")], + locations: [0, 1] + ) refreshTableViewHeight() } func refreshTableViewHeight() { let contentHeight = settingsTableView.contentSize.height guard contentHeight > 0 else { return } - - let tabBarHeight: CGFloat = 56 + kSafeBottomMargin - let maxHeight = bounds.height - tabBarHeight - settingsCardView.frame.minY - - if contentHeight <= maxHeight { - settingsCardHeightConstraint?.constant = contentHeight - settingsTableView.isScrollEnabled = false - } else { - settingsCardHeightConstraint?.constant = maxHeight - settingsTableView.isScrollEnabled = true - } + settingsCardHeightConstraint?.constant = contentHeight + settingsTableView.isScrollEnabled = false } } // MARK: - MineMenuCell final class MineMenuCell: UITableViewCell { - + func configure(_ model: MineMenuItem) { iconImageView.image = UIImage(named: model.icon) titleLabel.text = model.title } - + private func setupSubviews() { contentView.addSubview(iconImageView) contentView.addSubview(titleLabel) @@ -377,7 +359,7 @@ final class MineMenuCell: UITableViewCell { .left(14).right().bottom() .height(0.5) } - + var showDivider: Bool = true { didSet { dividerLine.isHidden = !showDivider } } @@ -419,15 +401,4 @@ final class MineMenuCell: UITableViewCell { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - override func awakeFromNib() { - super.awakeFromNib() - // Initialization code - } - - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state - } } diff --git a/QuickLocation/Section/Mine/MineViewController.swift b/QuickLocation/Section/Mine/MineViewController.swift index 5415e901..504e38a3 100644 --- a/QuickLocation/Section/Mine/MineViewController.swift +++ b/QuickLocation/Section/Mine/MineViewController.swift @@ -15,16 +15,15 @@ final class MineViewController: BaseViewController { override var isNavigationBarHidden: Bool { true } override var preferredStatusBarStyle: UIStatusBarStyle { .default } - // MARK: - Properties fileprivate var rootView: MineView! - + private var tableView: UITableView { rootView.settingsTableView } - - private let viewModel = MineViewModel() - // MARK: - Lifecycle + private let viewModel = MineViewModel() + private var groupKey: String = "" + override func loadView() { rootView = MineView(frame: UIScreen.main.bounds) view = rootView @@ -34,81 +33,76 @@ final class MineViewController: BaseViewController { super.viewDidLoad() bindViewModel() reactiveAction() - - rootView.avatarImageView.image = AppContextManager.shared.avaterIcon - rootView.nameLabel.text = AppContextManager.shared.name - rootView.idLabel.text = "ID:" + AppContextManager.shared.userId - viewModel.loadMenuData() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) requestUserInfo() + requestGroupMembers() } - // MARK: - Bindings private func bindViewModel() { viewModel.output.sectionedItems .bind(to: tableView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) - + tableView.rx.modelSelected(MineMenuItem.self) .subscribe(viewModel.cellAction.inputs) .disposed(by: disposeBag) - - viewModel.userName - .bind(to: rootView.nameLabel.rx.text) - .disposed(by: disposeBag) - - viewModel.userID - .bind(to: rootView.idLabel.rx.text) - .disposed(by: disposeBag) } private func reactiveAction() { rootView.vipActivateButton.rx.tap - .subscribe(onNext: { [weak self] in + .subscribe(onNext: { _ in AppRouter.push(Route.vipRecharge) }) .disposed(by: disposeBag) - rootView.editButton.rx.tap - .subscribe(onNext: { [weak self] in - // TODO: Navigate to edit profile - }) - .disposed(by: disposeBag) + rootView.photoWall.onCopyID = { id in + UIPasteboard.general.string = id + DLToast.showSuccess(text: "已复制") + } - rootView.copyButton.rx.tap - .subscribe(onNext: { [weak self] in - guard let id = self?.rootView.idLabel.text else { return } - UIPasteboard.general.string = id.replacingOccurrences(of: "ID:", with: "") - DLToast.showSuccess(text: "已复制") - }) - .disposed(by: disposeBag) + rootView.photoWall.onTapSelfAvatar = { + AppRouter.push(Route.myProfile) + } + + rootView.onQuickTap = { [weak self] title in + self?.viewModel.handleMenu(MineMenuItem(icon: "", title: title)) + } } - - // MARK: - UITableViewDataSource - lazy private var dataSource: RxTableViewSectionedReloadDataSource = { - return RxTableViewSectionedReloadDataSource( + + private lazy var dataSource: RxTableViewSectionedReloadDataSource = { + RxTableViewSectionedReloadDataSource( configureCell: { (_, tableView, indexPath, model) in let cell: MineMenuCell = tableView.dequeueReusableCell(for: indexPath) cell.configure(model) return cell }) }() - - // MARK: - API + private func requestUserInfo() { UserService.userInfo().subscribe { response in guard let model = response.model else { return } if AppContextManager.shared.saveAccount(model) { - self.rootView.avatarImageView.image = model.userIcon - self.rootView.nameLabel.text = model.name - self.rootView.idLabel.text = "ID:\(model.uid ?? "")" self.rootView.setupData(model) } - }.disposed(by: disposeBag) } + + private func requestGroupMembers() { + GroupService.groupInfo().subscribe(onNext: { [weak self] response in + guard let self = self, let model = response.model else { return } + self.groupKey = model.default_group_key + let sorted = self.viewModel.sortedMembers(model.select_group_employee, groupKey: model.default_group_key) + // 圈主 id:group_key 含 userId + let ownerId = sorted.first(where: { model.default_group_key.contains($0.user_id) })?.user_id ?? "" + self.rootView.photoWall.reload( + members: sorted, + ownerUserId: ownerId, + preferUserId: AppContextManager.shared.userId + ) + }).disposed(by: disposeBag) + } } diff --git a/QuickLocation/Section/Mine/MineViewModel.swift b/QuickLocation/Section/Mine/MineViewModel.swift index 5e2e25b1..98c4ec44 100644 --- a/QuickLocation/Section/Mine/MineViewModel.swift +++ b/QuickLocation/Section/Mine/MineViewModel.swift @@ -14,49 +14,81 @@ class MineViewModel { struct Output { var sectionedItems: Observable<[MineListSectionModel]> } - + let output: Output - + private var disposeBag = DisposeBag() private let sectionedItems = PublishSubject<[MineListSectionModel]>() - + let userAvatar = PublishSubject() let userName = PublishSubject() let userID = PublishSubject() + /// 设置列表(设计稿) let menuItems: [MineMenuItem] = [ MineMenuItem(icon: "Mine/account_security", title: "账户与安全"), -// MineMenuItem(icon: "Mine/status_setting", title: "状态设置"), MineMenuItem(icon: "Mine/emergency_contact", title: "紧急联系人"), MineMenuItem(icon: "Mine/customer_service", title: "在线客服"), - MineMenuItem(icon: "Mine/privacy_agreement", title: "隐私与协议"), - MineMenuItem(icon: "Mine/permission_check", title: "权限检测") + MineMenuItem(icon: "Mine/menu_about", title: "关于我们") + ] + + /// 快捷入口 + let quickItems: [MineMenuItem] = [ + MineMenuItem(icon: "Mine/quick_permission", title: "权限检测"), + MineMenuItem(icon: "Mine/quick_data_security", title: "数据安全"), + MineMenuItem(icon: "Mine/quick_status", title: "状态设置"), + MineMenuItem(icon: "Mine/quick_privacy", title: "隐私设置") ] lazy var cellAction: Action = { this in return Action { model in - switch model.title { - case "账户与安全": - AppRouter.push(Route.account) - case "紧急联系人": - AppRouter.push(Route.emergencyContact) - case "隐私与协议": - AppRouter.push(Route.privacyPolicy) - case "在线客服": - this.requestWechatService() - case "权限检测": - AppRouter.push(Route.checkPermission) - default: - break - } + this.handleMenu(model) return .empty() } }(self) - + + func handleMenu(_ model: MineMenuItem) { + switch model.title { + case "账户与安全": + AppRouter.push(Route.account) + case "紧急联系人": + AppRouter.push(Route.emergencyContact) + case "隐私与协议", "隐私设置": + AppRouter.push(Route.privacyPolicy) + case "在线客服": + requestWechatService() + case "权限检测": + AppRouter.push(Route.checkPermission) + case "数据安全", "状态设置", "关于我们": + DLToast.show(text: "敬请期待") + default: + break + } + } + func loadMenuData() { sectionedItems.onNext(menuItems.mapSection()) } - + + /// 圈主 → 自己 → 在线(对齐首页) + func sortedMembers(_ list: [GroupMemberModel], groupKey: String) -> [GroupMemberModel] { + list.enumerated() + .sorted { lhs, rhs in + let lp = memberSortPriority(lhs.element, groupKey: groupKey) + let rp = memberSortPriority(rhs.element, groupKey: groupKey) + if lp != rp { return lp < rp } + return lhs.offset < rhs.offset + } + .map(\.element) + } + + private func memberSortPriority(_ member: GroupMemberModel, groupKey: String) -> Int { + if !groupKey.isEmpty, groupKey.contains(member.user_id) { return 0 } + if member.user_id == AppContextManager.shared.userId { return 1 } + if member.is_online { return 2 } + return 3 + } + func requestWechatService() { DLToast.showLoading() SystemService.wechatService().subscribe(onNext: { response in @@ -70,8 +102,7 @@ class MineViewModel { WXApi.send(req) }, onError: { _ in }).disposed(by: disposeBag) } - - // MARK: - Init + init() { output = Output( sectionedItems: sectionedItems.asObservable() diff --git a/QuickLocation/UIKit/BaseNavigationView.swift b/QuickLocation/UIKit/BaseNavigationView.swift index a404c18c..f2dace10 100644 --- a/QuickLocation/UIKit/BaseNavigationView.swift +++ b/QuickLocation/UIKit/BaseNavigationView.swift @@ -36,8 +36,8 @@ class BaseNavigationView: UIView { backBtn.layoutChain .centerY(titleLabel) .left(15) - .width(24) - .height(24) + .width(36) + .height(36) titleLabel.layoutChain .top(kStatusBarHeight + 12)