commit c2df67d32f88af28deaf981e4542fd7f5fb45ce6 Author: cc Date: Sun Jul 27 12:33:06 2025 +0800 基础功能提交 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8fd0c89 --- /dev/null +++ b/.gitignore @@ -0,0 +1,152 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +Packages/ +Package.pins +Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +.swiftpm + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repository. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +# Mac OS X +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# 自定义排除规则 +# 排除LocationHelper.swift文件 +IOS_study/Library/LocationHelper.swift + +# 排除编译生成的文件 +*.o +*.a +*.so +*.dylib +*.framework +*.app +*.ipa +*.dSYM +*.xcarchive + +# 排除Xcode用户特定文件 +*.xcuserstate +*.xcuserdata/ +*.xcworkspace/xcuserdata/ + +# 排除临时文件 +*.tmp +*.temp +*.swp +*.swo +*~ + +# 排除日志文件 +*.log + +# 排除配置文件中的敏感信息 +*.plist +!IOS-study-Info.plist + +# 排除图片文件(如果需要) +# IOS_study/Img/ \ No newline at end of file diff --git a/IOS-study-Info.plist b/IOS-study-Info.plist new file mode 100644 index 0000000..50a6972 --- /dev/null +++ b/IOS-study-Info.plist @@ -0,0 +1,13 @@ + + + + + LSApplicationQueriesSchemes + + maps + iosamap + baidumap + qqmap + + + diff --git a/IOS_study.xcodeproj/project.pbxproj b/IOS_study.xcodeproj/project.pbxproj new file mode 100644 index 0000000..df9fce1 --- /dev/null +++ b/IOS_study.xcodeproj/project.pbxproj @@ -0,0 +1,750 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 940E13EB2E2E06F70026F8A5 /* MJRefresh in Frameworks */ = {isa = PBXBuildFile; productRef = 940E13EA2E2E06F70026F8A5 /* MJRefresh */; }; + 940E13EE2E2E07110026F8A5 /* LeanCloud in Frameworks */ = {isa = PBXBuildFile; productRef = 940E13ED2E2E07110026F8A5 /* LeanCloud */; }; + 940E13F12E2E07300026F8A5 /* DotLottie in Frameworks */ = {isa = PBXBuildFile; productRef = 940E13F02E2E07300026F8A5 /* DotLottie */; }; + 940E13F42E2E07470026F8A5 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 940E13F32E2E07470026F8A5 /* Kingfisher */; }; + 940E13F82E2F42000026F8A5 /* CompanyGalleryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 940E13F72E2F42000026F8A5 /* CompanyGalleryView.swift */; }; + 940E13FA2E2FA2660026F8A5 /* CompanyGalleryORView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 940E13F92E2FA2660026F8A5 /* CompanyGalleryORView.swift */; }; + 940E13FC2E2FA63B0026F8A5 /* PinchZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 940E13FB2E2FA63B0026F8A5 /* PinchZoomView.swift */; }; + 941830A12E252FA90004042F /* DBUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 941830A02E252FA90004042F /* DBUser.swift */; }; + 9418310C2E2533520004042F /* UserManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9418310B2E2533520004042F /* UserManager.swift */; }; + 941831EF2E25E6AC0004042F /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 941831EE2E25E6AC0004042F /* ProfileView.swift */; }; + 941831F12E25E6F10004042F /* AuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 941831F02E25E6F10004042F /* AuthViewModel.swift */; }; + 941831F32E2634BF0004042F /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 941831F22E2634BF0004042F /* ProfileViewModel.swift */; }; + 941831F52E2638080004042F /* PostCompanyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 941831F42E2638070004042F /* PostCompanyView.swift */; }; + 941831F72E26626F0004042F /* StorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 941831F62E26626F0004042F /* StorageManager.swift */; }; + 943D8EB62E29E2870079191A /* ProfileView+ComUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943D8EB52E29E2870079191A /* ProfileView+ComUI.swift */; }; + 9441C9742E323A7B00805BF3 /* PLJobsView+JobUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9441C9732E323A7B00805BF3 /* PLJobsView+JobUI.swift */; }; + 9441C9762E325A7E00805BF3 /* Reportmanager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9441C9752E325A7E00805BF3 /* Reportmanager.swift */; }; + 9441C9782E325B4800805BF3 /* Report.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9441C9772E325B4800805BF3 /* Report.swift */; }; + 9441C9B82E33432500805BF3 /* PLJobView+RevUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9441C9B72E33432500805BF3 /* PLJobView+RevUI.swift */; }; + 9441C9BA2E3345D100805BF3 /* PostReviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9441C9B92E3345D100805BF3 /* PostReviewView.swift */; }; + 9441C9BD2E33462000805BF3 /* ReviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9441C9BC2E33462000805BF3 /* ReviewViewModel.swift */; }; + 9441C9BF2E33465400805BF3 /* Review.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9441C9BE2E33465400805BF3 /* Review.swift */; }; + 9474AFCA2E14CAFC004AA9AB /* PLJobManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474AFC92E14CAFC004AA9AB /* PLJobManager.swift */; }; + 9474AFCC2E155E22004AA9AB /* LeanCloud+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474AFCB2E155E22004AA9AB /* LeanCloud+.swift */; }; + 94790B4C2E1179A600BF0A09 /* CJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B342E1179A600BF0A09 /* CJob.swift */; }; + 94790B4D2E1179A600BF0A09 /* CUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B352E1179A600BF0A09 /* CUser.swift */; }; + 94790B4E2E1179A600BF0A09 /* GView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B362E1179A600BF0A09 /* GView.swift */; }; + 94790B4F2E1179A600BF0A09 /* View+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B382E1179A600BF0A09 /* View+.swift */; }; + 94790B502E1179A600BF0A09 /* View+Label.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B392E1179A600BF0A09 /* View+Label.swift */; }; + 94790B512E1179A600BF0A09 /* View+Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B3A2E1179A600BF0A09 /* View+Style.swift */; }; + 94790B522E1179A600BF0A09 /* OverflowGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B3E2E1179A600BF0A09 /* OverflowGrid.swift */; }; + 94790B532E1179A600BF0A09 /* PLJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B402E1179A600BF0A09 /* PLJob.swift */; }; + 94790B542E1179A600BF0A09 /* IOS_studyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B452E1179A600BF0A09 /* IOS_studyApp.swift */; }; + 94790B552E1179A600BF0A09 /* PLJobCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B462E1179A600BF0A09 /* PLJobCellView.swift */; }; + 94790B562E1179A600BF0A09 /* PLJobsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B472E1179A600BF0A09 /* PLJobsView.swift */; }; + 94790B572E1179A600BF0A09 /* PLJobView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B482E1179A600BF0A09 /* PLJobView.swift */; }; + 94790B582E1179A600BF0A09 /* PostPLJobIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B492E1179A600BF0A09 /* PostPLJobIView.swift */; }; + 94790B592E1179A600BF0A09 /* TaBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B4A2E1179A600BF0A09 /* TaBarView.swift */; }; + 94790B5B2E1179A600BF0A09 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 94790B422E1179A600BF0A09 /* Preview Assets.xcassets */; }; + 94790B5C2E1179A600BF0A09 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 94790B442E1179A600BF0A09 /* Assets.xcassets */; }; + 94790B5E2E117AD800BF0A09 /* PLJobViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B5D2E117AD800BF0A09 /* PLJobViewModel.swift */; }; + 94790B602E11846F00BF0A09 /* Foundation+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B5F2E11846F00BF0A09 /* Foundation+.swift */; }; + 94790B622E118B3600BF0A09 /* PostPLJobIView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B612E118B3600BF0A09 /* PostPLJobIView+.swift */; }; + 94790B642E118CB400BF0A09 /* HUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B632E118CB400BF0A09 /* HUDView.swift */; }; + 94790B662E129CC500BF0A09 /* GType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B652E129CC500BF0A09 /* GType.swift */; }; + 94790B682E137DB300BF0A09 /* PostJobView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B672E137DB300BF0A09 /* PostJobView.swift */; }; + 94790B6A2E13820500BF0A09 /* TaBarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B692E13820500BF0A09 /* TaBarViewModel.swift */; }; + 94790B6C2E13879400BF0A09 /* PLJobView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94790B6B2E13879300BF0A09 /* PLJobView+.swift */; }; + 949F8B282E30C5210013AB3A /* ProfileViewModel+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 949F8B272E30C5210013AB3A /* ProfileViewModel+.swift */; }; + 949F8B2A2E30CCFB0013AB3A /* ProfileView+JobUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 949F8B292E30CCFB0013AB3A /* ProfileView+JobUI.swift */; }; + 94F008672E16345100989E46 /* GFunc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F008662E16345100989E46 /* GFunc.swift */; }; + 94F008692E16749B00989E46 /* RESTManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F008682E16749B00989E46 /* RESTManager.swift */; }; + 94F0E24E2E1CFA00003406F0 /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F0E24D2E1CFA00003406F0 /* NetworkMonitor.swift */; }; + 94F0E2562E1F6530003406F0 /* LocationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F0E2552E1F6530003406F0 /* LocationHelper.swift */; }; + 94F0E2582E1F87D2003406F0 /* OF.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F0E2572E1F87D2003406F0 /* OF.swift */; }; + 94F0E25A2E1F996A003406F0 /* PLJobsView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F0E2592E1F996A003406F0 /* PLJobsView+.swift */; }; + 94F0E25D2E1FC171003406F0 /* SignInPhoneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F0E25C2E1FC170003406F0 /* SignInPhoneView.swift */; }; + 94F0E25F2E1FC186003406F0 /* SignInPhoneViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F0E25E2E1FC186003406F0 /* SignInPhoneViewModel.swift */; }; + 94F0E2622E1FC2D5003406F0 /* SettginsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F0E2612E1FC2D5003406F0 /* SettginsView.swift */; }; + 94F0E2642E1FDAB2003406F0 /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F0E2632E1FDAB2003406F0 /* AuthManager.swift */; }; + 94F0E2662E1FFE58003406F0 /* SignInPhoneView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F0E2652E1FFE58003406F0 /* SignInPhoneView+.swift */; }; + 94F0E2682E23FB7E003406F0 /* AgreeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F0E2672E23FB7E003406F0 /* AgreeView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 940E13F72E2F42000026F8A5 /* CompanyGalleryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompanyGalleryView.swift; sourceTree = ""; }; + 940E13F92E2FA2660026F8A5 /* CompanyGalleryORView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompanyGalleryORView.swift; sourceTree = ""; }; + 940E13FB2E2FA63B0026F8A5 /* PinchZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinchZoomView.swift; sourceTree = ""; }; + 941830A02E252FA90004042F /* DBUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBUser.swift; sourceTree = ""; }; + 9418310B2E2533520004042F /* UserManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserManager.swift; sourceTree = ""; }; + 941831EE2E25E6AC0004042F /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; + 941831F02E25E6F10004042F /* AuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewModel.swift; sourceTree = ""; }; + 941831F22E2634BF0004042F /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; }; + 941831F42E2638070004042F /* PostCompanyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCompanyView.swift; sourceTree = ""; }; + 941831F62E26626F0004042F /* StorageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageManager.swift; sourceTree = ""; }; + 943D8EB52E29E2870079191A /* ProfileView+ComUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+ComUI.swift"; sourceTree = ""; }; + 9441C9732E323A7B00805BF3 /* PLJobsView+JobUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PLJobsView+JobUI.swift"; sourceTree = ""; }; + 9441C9752E325A7E00805BF3 /* Reportmanager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reportmanager.swift; sourceTree = ""; }; + 9441C9772E325B4800805BF3 /* Report.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Report.swift; sourceTree = ""; }; + 9441C9B72E33432500805BF3 /* PLJobView+RevUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PLJobView+RevUI.swift"; sourceTree = ""; }; + 9441C9B92E3345D100805BF3 /* PostReviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostReviewView.swift; sourceTree = ""; }; + 9441C9BC2E33462000805BF3 /* ReviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewViewModel.swift; sourceTree = ""; }; + 9441C9BE2E33465400805BF3 /* Review.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Review.swift; sourceTree = ""; }; + 9474AFC92E14CAFC004AA9AB /* PLJobManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PLJobManager.swift; sourceTree = ""; }; + 9474AFCB2E155E22004AA9AB /* LeanCloud+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LeanCloud+.swift"; sourceTree = ""; }; + 94790B342E1179A600BF0A09 /* CJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CJob.swift; sourceTree = ""; }; + 94790B352E1179A600BF0A09 /* CUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CUser.swift; sourceTree = ""; }; + 94790B362E1179A600BF0A09 /* GView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GView.swift; sourceTree = ""; }; + 94790B382E1179A600BF0A09 /* View+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+.swift"; sourceTree = ""; }; + 94790B392E1179A600BF0A09 /* View+Label.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Label.swift"; sourceTree = ""; }; + 94790B3A2E1179A600BF0A09 /* View+Style.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Style.swift"; sourceTree = ""; }; + 94790B3E2E1179A600BF0A09 /* OverflowGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverflowGrid.swift; sourceTree = ""; }; + 94790B402E1179A600BF0A09 /* PLJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PLJob.swift; sourceTree = ""; }; + 94790B422E1179A600BF0A09 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 94790B442E1179A600BF0A09 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 94790B452E1179A600BF0A09 /* IOS_studyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOS_studyApp.swift; sourceTree = ""; }; + 94790B462E1179A600BF0A09 /* PLJobCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PLJobCellView.swift; sourceTree = ""; }; + 94790B472E1179A600BF0A09 /* PLJobsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PLJobsView.swift; sourceTree = ""; }; + 94790B482E1179A600BF0A09 /* PLJobView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PLJobView.swift; sourceTree = ""; }; + 94790B492E1179A600BF0A09 /* PostPLJobIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostPLJobIView.swift; sourceTree = ""; }; + 94790B4A2E1179A600BF0A09 /* TaBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaBarView.swift; sourceTree = ""; }; + 94790B5D2E117AD800BF0A09 /* PLJobViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PLJobViewModel.swift; sourceTree = ""; }; + 94790B5F2E11846F00BF0A09 /* Foundation+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Foundation+.swift"; sourceTree = ""; }; + 94790B612E118B3600BF0A09 /* PostPLJobIView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostPLJobIView+.swift"; sourceTree = ""; }; + 94790B632E118CB400BF0A09 /* HUDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HUDView.swift; sourceTree = ""; }; + 94790B652E129CC500BF0A09 /* GType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GType.swift; sourceTree = ""; }; + 94790B672E137DB300BF0A09 /* PostJobView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostJobView.swift; sourceTree = ""; }; + 94790B692E13820500BF0A09 /* TaBarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaBarViewModel.swift; sourceTree = ""; }; + 94790B6B2E13879300BF0A09 /* PLJobView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PLJobView+.swift"; sourceTree = ""; }; + 947A93B32DEEE882002E0937 /* IOS_study.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IOS_study.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 949F8B272E30C5210013AB3A /* ProfileViewModel+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileViewModel+.swift"; sourceTree = ""; }; + 949F8B292E30CCFB0013AB3A /* ProfileView+JobUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+JobUI.swift"; sourceTree = ""; }; + 94F008662E16345100989E46 /* GFunc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GFunc.swift; sourceTree = ""; }; + 94F008682E16749B00989E46 /* RESTManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTManager.swift; sourceTree = ""; }; + 94F0E24D2E1CFA00003406F0 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = ""; }; + 94F0E2542E1F5F15003406F0 /* IOS-study-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "IOS-study-Info.plist"; sourceTree = SOURCE_ROOT; }; + 94F0E2552E1F6530003406F0 /* LocationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationHelper.swift; sourceTree = ""; }; + 94F0E2572E1F87D2003406F0 /* OF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OF.swift; sourceTree = ""; }; + 94F0E2592E1F996A003406F0 /* PLJobsView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PLJobsView+.swift"; sourceTree = ""; }; + 94F0E25C2E1FC170003406F0 /* SignInPhoneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInPhoneView.swift; sourceTree = ""; }; + 94F0E25E2E1FC186003406F0 /* SignInPhoneViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInPhoneViewModel.swift; sourceTree = ""; }; + 94F0E2612E1FC2D5003406F0 /* SettginsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettginsView.swift; sourceTree = ""; }; + 94F0E2632E1FDAB2003406F0 /* AuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = ""; }; + 94F0E2652E1FFE58003406F0 /* SignInPhoneView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SignInPhoneView+.swift"; sourceTree = ""; }; + 94F0E2672E23FB7E003406F0 /* AgreeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgreeView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 947A93B02DEEE882002E0937 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 940E13F42E2E07470026F8A5 /* Kingfisher in Frameworks */, + 940E13EE2E2E07110026F8A5 /* LeanCloud in Frameworks */, + 940E13EB2E2E06F70026F8A5 /* MJRefresh in Frameworks */, + 940E13F12E2E07300026F8A5 /* DotLottie in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9441C9BB2E3345FE00805BF3 /* Review */ = { + isa = PBXGroup; + children = ( + 9441C9BC2E33462000805BF3 /* ReviewViewModel.swift */, + ); + path = Review; + sourceTree = ""; + }; + 9474AFC52E14C6DC004AA9AB /* Job */ = { + isa = PBXGroup; + children = ( + 94790B5D2E117AD800BF0A09 /* PLJobViewModel.swift */, + 94790B482E1179A600BF0A09 /* PLJobView.swift */, + 9441C9732E323A7B00805BF3 /* PLJobsView+JobUI.swift */, + 94790B6B2E13879300BF0A09 /* PLJobView+.swift */, + ); + path = Job; + sourceTree = ""; + }; + 9474AFC62E14C6EB004AA9AB /* Post */ = { + isa = PBXGroup; + children = ( + 94790B612E118B3600BF0A09 /* PostPLJobIView+.swift */, + 94790B492E1179A600BF0A09 /* PostPLJobIView.swift */, + 94790B672E137DB300BF0A09 /* PostJobView.swift */, + 941831F42E2638070004042F /* PostCompanyView.swift */, + 940E13F72E2F42000026F8A5 /* CompanyGalleryView.swift */, + 9441C9B92E3345D100805BF3 /* PostReviewView.swift */, + 940E13F92E2FA2660026F8A5 /* CompanyGalleryORView.swift */, + ); + path = Post; + sourceTree = ""; + }; + 9474AFC72E14C724004AA9AB /* Jobs */ = { + isa = PBXGroup; + children = ( + 94790B472E1179A600BF0A09 /* PLJobsView.swift */, + 94F0E2592E1F996A003406F0 /* PLJobsView+.swift */, + 94790B462E1179A600BF0A09 /* PLJobCellView.swift */, + ); + path = Jobs; + sourceTree = ""; + }; + 9474AFC82E14CAC8004AA9AB /* Manager */ = { + isa = PBXGroup; + children = ( + 9418310B2E2533520004042F /* UserManager.swift */, + 9441C9752E325A7E00805BF3 /* Reportmanager.swift */, + 941831F62E26626F0004042F /* StorageManager.swift */, + 94F0E2632E1FDAB2003406F0 /* AuthManager.swift */, + 9474AFC92E14CAFC004AA9AB /* PLJobManager.swift */, + 94F008682E16749B00989E46 /* RESTManager.swift */, + ); + path = Manager; + sourceTree = ""; + }; + 94790B372E1179A600BF0A09 /* Constant */ = { + isa = PBXGroup; + children = ( + 94790B342E1179A600BF0A09 /* CJob.swift */, + 94790B352E1179A600BF0A09 /* CUser.swift */, + 94790B362E1179A600BF0A09 /* GView.swift */, + 94F008662E16345100989E46 /* GFunc.swift */, + 94790B652E129CC500BF0A09 /* GType.swift */, + ); + path = Constant; + sourceTree = ""; + }; + 94790B3B2E1179A600BF0A09 /* Extension */ = { + isa = PBXGroup; + children = ( + 94790B5F2E11846F00BF0A09 /* Foundation+.swift */, + 94790B382E1179A600BF0A09 /* View+.swift */, + 9474AFCB2E155E22004AA9AB /* LeanCloud+.swift */, + 94790B392E1179A600BF0A09 /* View+Label.swift */, + 94790B3A2E1179A600BF0A09 /* View+Style.swift */, + ); + path = Extension; + sourceTree = ""; + }; + 94790B3F2E1179A600BF0A09 /* Library */ = { + isa = PBXGroup; + children = ( + 94790B632E118CB400BF0A09 /* HUDView.swift */, + 94790B3E2E1179A600BF0A09 /* OverflowGrid.swift */, + 94F0E24D2E1CFA00003406F0 /* NetworkMonitor.swift */, + 94F0E2552E1F6530003406F0 /* LocationHelper.swift */, + 940E13FB2E2FA63B0026F8A5 /* PinchZoomView.swift */, + ); + path = Library; + sourceTree = ""; + }; + 94790B412E1179A600BF0A09 /* Model */ = { + isa = PBXGroup; + children = ( + 94790B402E1179A600BF0A09 /* PLJob.swift */, + 9441C9BE2E33465400805BF3 /* Review.swift */, + 9441C9772E325B4800805BF3 /* Report.swift */, + 94F0E2572E1F87D2003406F0 /* OF.swift */, + 941830A02E252FA90004042F /* DBUser.swift */, + ); + path = Model; + sourceTree = ""; + }; + 94790B432E1179A600BF0A09 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 94790B422E1179A600BF0A09 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 94790B4B2E1179A600BF0A09 /* IOS_study */ = { + isa = PBXGroup; + children = ( + 94F0E2542E1F5F15003406F0 /* IOS-study-Info.plist */, + 94790B452E1179A600BF0A09 /* IOS_studyApp.swift */, + 94790B4A2E1179A600BF0A09 /* TaBarView.swift */, + 94790B692E13820500BF0A09 /* TaBarViewModel.swift */, + 9441C9BB2E3345FE00805BF3 /* Review */, + 94790B412E1179A600BF0A09 /* Model */, + 94F0E2602E1FC2AD003406F0 /* Profile */, + 94F0E25B2E1FC14A003406F0 /* Auth */, + 9474AFC82E14CAC8004AA9AB /* Manager */, + 9474AFC72E14C724004AA9AB /* Jobs */, + 9474AFC52E14C6DC004AA9AB /* Job */, + 9474AFC62E14C6EB004AA9AB /* Post */, + 94790B3B2E1179A600BF0A09 /* Extension */, + 94790B372E1179A600BF0A09 /* Constant */, + 94790B442E1179A600BF0A09 /* Assets.xcassets */, + 94790B3F2E1179A600BF0A09 /* Library */, + 94790B432E1179A600BF0A09 /* Preview Content */, + ); + path = IOS_study; + sourceTree = ""; + }; + 947A93AA2DEEE882002E0937 = { + isa = PBXGroup; + children = ( + 94790B4B2E1179A600BF0A09 /* IOS_study */, + 947A93B42DEEE882002E0937 /* Products */, + ); + sourceTree = ""; + }; + 947A93B42DEEE882002E0937 /* Products */ = { + isa = PBXGroup; + children = ( + 947A93B32DEEE882002E0937 /* IOS_study.app */, + ); + name = Products; + sourceTree = ""; + }; + 94F0E25B2E1FC14A003406F0 /* Auth */ = { + isa = PBXGroup; + children = ( + 94F0E25E2E1FC186003406F0 /* SignInPhoneViewModel.swift */, + 94F0E25C2E1FC170003406F0 /* SignInPhoneView.swift */, + 94F0E2652E1FFE58003406F0 /* SignInPhoneView+.swift */, + 94F0E2672E23FB7E003406F0 /* AgreeView.swift */, + 941831F02E25E6F10004042F /* AuthViewModel.swift */, + ); + path = Auth; + sourceTree = ""; + }; + 94F0E2602E1FC2AD003406F0 /* Profile */ = { + isa = PBXGroup; + children = ( + 941831F22E2634BF0004042F /* ProfileViewModel.swift */, + 949F8B272E30C5210013AB3A /* ProfileViewModel+.swift */, + 941831EE2E25E6AC0004042F /* ProfileView.swift */, + 94F0E2612E1FC2D5003406F0 /* SettginsView.swift */, + 949F8B292E30CCFB0013AB3A /* ProfileView+JobUI.swift */, + 9441C9B72E33432500805BF3 /* PLJobView+RevUI.swift */, + 943D8EB52E29E2870079191A /* ProfileView+ComUI.swift */, + ); + path = Profile; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 947A93B22DEEE882002E0937 /* IOS_study */ = { + isa = PBXNativeTarget; + buildConfigurationList = 947A93C12DEEE885002E0937 /* Build configuration list for PBXNativeTarget "IOS_study" */; + buildPhases = ( + 947A93AF2DEEE882002E0937 /* Sources */, + 947A93B02DEEE882002E0937 /* Frameworks */, + 947A93B12DEEE882002E0937 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = IOS_study; + packageProductDependencies = ( + 940E13EA2E2E06F70026F8A5 /* MJRefresh */, + 940E13ED2E2E07110026F8A5 /* LeanCloud */, + 940E13F02E2E07300026F8A5 /* DotLottie */, + 940E13F32E2E07470026F8A5 /* Kingfisher */, + ); + productName = IOS_study; + productReference = 947A93B32DEEE882002E0937 /* IOS_study.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 947A93AB2DEEE882002E0937 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1620; + LastUpgradeCheck = 1620; + TargetAttributes = { + 947A93B22DEEE882002E0937 = { + CreatedOnToolsVersion = 16.2; + }; + }; + }; + buildConfigurationList = 947A93AE2DEEE882002E0937 /* Build configuration list for PBXProject "IOS_study" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 947A93AA2DEEE882002E0937; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 940E13E92E2E06F70026F8A5 /* XCRemoteSwiftPackageReference "MJRefresh" */, + 940E13EC2E2E07110026F8A5 /* XCRemoteSwiftPackageReference "swift-sdk" */, + 940E13EF2E2E07300026F8A5 /* XCRemoteSwiftPackageReference "dotlottie-ios" */, + 940E13F22E2E07470026F8A5 /* XCRemoteSwiftPackageReference "Kingfisher" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 947A93B42DEEE882002E0937 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 947A93B22DEEE882002E0937 /* IOS_study */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 947A93B12DEEE882002E0937 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 94790B5B2E1179A600BF0A09 /* Preview Assets.xcassets in Resources */, + 94790B5C2E1179A600BF0A09 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 947A93AF2DEEE882002E0937 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 94F0E2582E1F87D2003406F0 /* OF.swift in Sources */, + 941831EF2E25E6AC0004042F /* ProfileView.swift in Sources */, + 94F0E2562E1F6530003406F0 /* LocationHelper.swift in Sources */, + 9418310C2E2533520004042F /* UserManager.swift in Sources */, + 94790B642E118CB400BF0A09 /* HUDView.swift in Sources */, + 94790B4C2E1179A600BF0A09 /* CJob.swift in Sources */, + 94790B662E129CC500BF0A09 /* GType.swift in Sources */, + 94790B4D2E1179A600BF0A09 /* CUser.swift in Sources */, + 94790B682E137DB300BF0A09 /* PostJobView.swift in Sources */, + 9441C9BF2E33465400805BF3 /* Review.swift in Sources */, + 94F0E2642E1FDAB2003406F0 /* AuthManager.swift in Sources */, + 9474AFCC2E155E22004AA9AB /* LeanCloud+.swift in Sources */, + 94790B4E2E1179A600BF0A09 /* GView.swift in Sources */, + 94F0E24E2E1CFA00003406F0 /* NetworkMonitor.swift in Sources */, + 94790B4F2E1179A600BF0A09 /* View+.swift in Sources */, + 94790B502E1179A600BF0A09 /* View+Label.swift in Sources */, + 9441C9762E325A7E00805BF3 /* Reportmanager.swift in Sources */, + 94F0E2622E1FC2D5003406F0 /* SettginsView.swift in Sources */, + 9441C9B82E33432500805BF3 /* PLJobView+RevUI.swift in Sources */, + 941831F52E2638080004042F /* PostCompanyView.swift in Sources */, + 9441C9BD2E33462000805BF3 /* ReviewViewModel.swift in Sources */, + 94F0E25D2E1FC171003406F0 /* SignInPhoneView.swift in Sources */, + 94790B512E1179A600BF0A09 /* View+Style.swift in Sources */, + 94790B522E1179A600BF0A09 /* OverflowGrid.swift in Sources */, + 94790B532E1179A600BF0A09 /* PLJob.swift in Sources */, + 94790B542E1179A600BF0A09 /* IOS_studyApp.swift in Sources */, + 9441C9BA2E3345D100805BF3 /* PostReviewView.swift in Sources */, + 940E13FC2E2FA63B0026F8A5 /* PinchZoomView.swift in Sources */, + 94F008672E16345100989E46 /* GFunc.swift in Sources */, + 9474AFCA2E14CAFC004AA9AB /* PLJobManager.swift in Sources */, + 941831F32E2634BF0004042F /* ProfileViewModel.swift in Sources */, + 949F8B282E30C5210013AB3A /* ProfileViewModel+.swift in Sources */, + 94F0E2682E23FB7E003406F0 /* AgreeView.swift in Sources */, + 94F0E2662E1FFE58003406F0 /* SignInPhoneView+.swift in Sources */, + 94790B552E1179A600BF0A09 /* PLJobCellView.swift in Sources */, + 9441C9782E325B4800805BF3 /* Report.swift in Sources */, + 9441C9742E323A7B00805BF3 /* PLJobsView+JobUI.swift in Sources */, + 941830A12E252FA90004042F /* DBUser.swift in Sources */, + 94F0E25A2E1F996A003406F0 /* PLJobsView+.swift in Sources */, + 94790B6A2E13820500BF0A09 /* TaBarViewModel.swift in Sources */, + 94790B6C2E13879400BF0A09 /* PLJobView+.swift in Sources */, + 940E13F82E2F42000026F8A5 /* CompanyGalleryView.swift in Sources */, + 941831F72E26626F0004042F /* StorageManager.swift in Sources */, + 94790B562E1179A600BF0A09 /* PLJobsView.swift in Sources */, + 943D8EB62E29E2870079191A /* ProfileView+ComUI.swift in Sources */, + 941831F12E25E6F10004042F /* AuthViewModel.swift in Sources */, + 94F008692E16749B00989E46 /* RESTManager.swift in Sources */, + 94790B622E118B3600BF0A09 /* PostPLJobIView+.swift in Sources */, + 949F8B2A2E30CCFB0013AB3A /* ProfileView+JobUI.swift in Sources */, + 94790B572E1179A600BF0A09 /* PLJobView.swift in Sources */, + 940E13FA2E2FA2660026F8A5 /* CompanyGalleryORView.swift in Sources */, + 94790B582E1179A600BF0A09 /* PostPLJobIView.swift in Sources */, + 94790B592E1179A600BF0A09 /* TaBarView.swift in Sources */, + 94790B602E11846F00BF0A09 /* Foundation+.swift in Sources */, + 94F0E25F2E1FC186003406F0 /* SignInPhoneViewModel.swift in Sources */, + 94790B5E2E117AD800BF0A09 /* PLJobViewModel.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 947A93BF2DEEE885002E0937 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 947A93C02DEEE885002E0937 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 947A93C22DEEE885002E0937 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"IOS_study/Preview Content\""; + DEVELOPMENT_TEAM = 6M7MAFWB7K; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "IOS-study-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = Boss_tdcat; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "仅添加照片,不会读取任何照片"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "cn.tdcat.IOS-study"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 947A93C32DEEE885002E0937 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"IOS_study/Preview Content\""; + DEVELOPMENT_TEAM = 6M7MAFWB7K; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "IOS-study-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = Boss_tdcat; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "仅添加照片,不会读取任何照片"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "cn.tdcat.IOS-study"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 947A93AE2DEEE882002E0937 /* Build configuration list for PBXProject "IOS_study" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 947A93BF2DEEE885002E0937 /* Debug */, + 947A93C02DEEE885002E0937 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 947A93C12DEEE885002E0937 /* Build configuration list for PBXNativeTarget "IOS_study" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 947A93C22DEEE885002E0937 /* Debug */, + 947A93C32DEEE885002E0937 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 940E13E92E2E06F70026F8A5 /* XCRemoteSwiftPackageReference "MJRefresh" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/CoderMJLee/MJRefresh.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.7.9; + }; + }; + 940E13EC2E2E07110026F8A5 /* XCRemoteSwiftPackageReference "swift-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/leancloud/swift-sdk.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 17.11.0; + }; + }; + 940E13EF2E2E07300026F8A5 /* XCRemoteSwiftPackageReference "dotlottie-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/LottieFiles/dotlottie-ios"; + requirement = { + branch = main; + kind = branch; + }; + }; + 940E13F22E2E07470026F8A5 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/onevcat/Kingfisher.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 8.5.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 940E13EA2E2E06F70026F8A5 /* MJRefresh */ = { + isa = XCSwiftPackageProductDependency; + package = 940E13E92E2E06F70026F8A5 /* XCRemoteSwiftPackageReference "MJRefresh" */; + productName = MJRefresh; + }; + 940E13ED2E2E07110026F8A5 /* LeanCloud */ = { + isa = XCSwiftPackageProductDependency; + package = 940E13EC2E2E07110026F8A5 /* XCRemoteSwiftPackageReference "swift-sdk" */; + productName = LeanCloud; + }; + 940E13F02E2E07300026F8A5 /* DotLottie */ = { + isa = XCSwiftPackageProductDependency; + package = 940E13EF2E2E07300026F8A5 /* XCRemoteSwiftPackageReference "dotlottie-ios" */; + productName = DotLottie; + }; + 940E13F32E2E07470026F8A5 /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = 940E13F22E2E07470026F8A5 /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 947A93AB2DEEE882002E0937 /* Project object */; +} diff --git a/IOS_study.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/IOS_study.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/IOS_study.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/IOS_study.xcodeproj/xcshareddata/xcschemes/IOS_study.xcscheme b/IOS_study.xcodeproj/xcshareddata/xcschemes/IOS_study.xcscheme new file mode 100644 index 0000000..fa7e53e --- /dev/null +++ b/IOS_study.xcodeproj/xcshareddata/xcschemes/IOS_study.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IOS_study/Assets.xcassets/AccentColor.colorset/Contents.json b/IOS_study/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..d352008 --- /dev/null +++ b/IOS_study/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,18 @@ +{ + "colors" : [ + { + "color" : { + "platform" : "universal", + "reference" : "systemIndigoColor" + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "localizable" : true + } +} diff --git a/IOS_study/Assets.xcassets/AppIcon.appiconset/Contents.json b/IOS_study/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..5d9d567 --- /dev/null +++ b/IOS_study/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,36 @@ +{ + "images" : [ + { + "filename" : "TDCAT..png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/AppIcon.appiconset/TDCAT..png b/IOS_study/Assets.xcassets/AppIcon.appiconset/TDCAT..png new file mode 100644 index 0000000..0b2c69a Binary files /dev/null and b/IOS_study/Assets.xcassets/AppIcon.appiconset/TDCAT..png differ diff --git a/IOS_study/Assets.xcassets/Contents.json b/IOS_study/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/IOS_study/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/aBG.colorset/Contents.json b/IOS_study/Assets.xcassets/aBG.colorset/Contents.json new file mode 100644 index 0000000..412aa5e --- /dev/null +++ b/IOS_study/Assets.xcassets/aBG.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "0.996" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.122", + "green" : "0.114", + "red" : "0.114" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/aBG2.colorset/Contents.json b/IOS_study/Assets.xcassets/aBG2.colorset/Contents.json new file mode 100644 index 0000000..78a50e2 --- /dev/null +++ b/IOS_study/Assets.xcassets/aBG2.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.965", + "green" : "0.961", + "red" : "0.961" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.078", + "green" : "0.075", + "red" : "0.075" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/aBGR.colorset/Contents.json b/IOS_study/Assets.xcassets/aBGR.colorset/Contents.json new file mode 100644 index 0000000..6eb0ef3 --- /dev/null +++ b/IOS_study/Assets.xcassets/aBGR.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.122", + "green" : "0.114", + "red" : "0.114" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "0.996" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/aTC 2.colorset/Contents.json b/IOS_study/Assets.xcassets/aTC 2.colorset/Contents.json new file mode 100644 index 0000000..29ead70 --- /dev/null +++ b/IOS_study/Assets.xcassets/aTC 2.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.200", + "green" : "0.200", + "red" : "0.200" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.753", + "green" : "0.753", + "red" : "0.753" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/aTC.colorset/Contents.json b/IOS_study/Assets.xcassets/aTC.colorset/Contents.json new file mode 100644 index 0000000..daaed74 --- /dev/null +++ b/IOS_study/Assets.xcassets/aTC.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.200", + "green" : "0.200", + "red" : "0.200" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.878", + "green" : "0.878", + "red" : "0.878" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/aTCR.colorset/Contents.json b/IOS_study/Assets.xcassets/aTCR.colorset/Contents.json new file mode 100644 index 0000000..54acf5f --- /dev/null +++ b/IOS_study/Assets.xcassets/aTCR.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.878", + "green" : "0.878", + "red" : "0.878" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.200", + "green" : "0.200", + "red" : "0.200" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/jobIcons/Contents.json b/IOS_study/Assets.xcassets/jobIcons/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/IOS_study/Assets.xcassets/jobIcons/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/jobIcons/benefit.imageset/Contents.json b/IOS_study/Assets.xcassets/jobIcons/benefit.imageset/Contents.json new file mode 100644 index 0000000..b5106d0 --- /dev/null +++ b/IOS_study/Assets.xcassets/jobIcons/benefit.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "benefit.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/jobIcons/benefit.imageset/benefit.png b/IOS_study/Assets.xcassets/jobIcons/benefit.imageset/benefit.png new file mode 100644 index 0000000..f36afaf Binary files /dev/null and b/IOS_study/Assets.xcassets/jobIcons/benefit.imageset/benefit.png differ diff --git a/IOS_study/Assets.xcassets/jobIcons/company.imageset/Contents.json b/IOS_study/Assets.xcassets/jobIcons/company.imageset/Contents.json new file mode 100644 index 0000000..3f022af --- /dev/null +++ b/IOS_study/Assets.xcassets/jobIcons/company.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "company.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/jobIcons/company.imageset/company.png b/IOS_study/Assets.xcassets/jobIcons/company.imageset/company.png new file mode 100644 index 0000000..b13eb9a Binary files /dev/null and b/IOS_study/Assets.xcassets/jobIcons/company.imageset/company.png differ diff --git a/IOS_study/Assets.xcassets/jobIcons/content.imageset/Contents.json b/IOS_study/Assets.xcassets/jobIcons/content.imageset/Contents.json new file mode 100644 index 0000000..8b7476d --- /dev/null +++ b/IOS_study/Assets.xcassets/jobIcons/content.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "content.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/jobIcons/content.imageset/content.png b/IOS_study/Assets.xcassets/jobIcons/content.imageset/content.png new file mode 100644 index 0000000..5b34d3d Binary files /dev/null and b/IOS_study/Assets.xcassets/jobIcons/content.imageset/content.png differ diff --git a/IOS_study/Assets.xcassets/jobIcons/employ.imageset/Contents.json b/IOS_study/Assets.xcassets/jobIcons/employ.imageset/Contents.json new file mode 100644 index 0000000..c0d31cf --- /dev/null +++ b/IOS_study/Assets.xcassets/jobIcons/employ.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "employ.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/jobIcons/employ.imageset/employ.png b/IOS_study/Assets.xcassets/jobIcons/employ.imageset/employ.png new file mode 100644 index 0000000..1c70ffe Binary files /dev/null and b/IOS_study/Assets.xcassets/jobIcons/employ.imageset/employ.png differ diff --git a/IOS_study/Assets.xcassets/jobIcons/leader.imageset/Contents.json b/IOS_study/Assets.xcassets/jobIcons/leader.imageset/Contents.json new file mode 100644 index 0000000..0991505 --- /dev/null +++ b/IOS_study/Assets.xcassets/jobIcons/leader.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "leader.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/jobIcons/leader.imageset/leader.png b/IOS_study/Assets.xcassets/jobIcons/leader.imageset/leader.png new file mode 100644 index 0000000..7da6987 Binary files /dev/null and b/IOS_study/Assets.xcassets/jobIcons/leader.imageset/leader.png differ diff --git a/IOS_study/Assets.xcassets/jobIcons/need.imageset/Contents.json b/IOS_study/Assets.xcassets/jobIcons/need.imageset/Contents.json new file mode 100644 index 0000000..24ff476 --- /dev/null +++ b/IOS_study/Assets.xcassets/jobIcons/need.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "need.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/jobIcons/need.imageset/need.png b/IOS_study/Assets.xcassets/jobIcons/need.imageset/need.png new file mode 100644 index 0000000..f7ff6b8 Binary files /dev/null and b/IOS_study/Assets.xcassets/jobIcons/need.imageset/need.png differ diff --git a/IOS_study/Assets.xcassets/jobIcons/station.imageset/Contents.json b/IOS_study/Assets.xcassets/jobIcons/station.imageset/Contents.json new file mode 100644 index 0000000..ec47dc7 --- /dev/null +++ b/IOS_study/Assets.xcassets/jobIcons/station.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "station.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/jobIcons/station.imageset/station.png b/IOS_study/Assets.xcassets/jobIcons/station.imageset/station.png new file mode 100644 index 0000000..c3cf318 Binary files /dev/null and b/IOS_study/Assets.xcassets/jobIcons/station.imageset/station.png differ diff --git a/IOS_study/Assets.xcassets/jobIcons/time.imageset/Contents.json b/IOS_study/Assets.xcassets/jobIcons/time.imageset/Contents.json new file mode 100644 index 0000000..3babb4d --- /dev/null +++ b/IOS_study/Assets.xcassets/jobIcons/time.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "time.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/jobIcons/time.imageset/time.png b/IOS_study/Assets.xcassets/jobIcons/time.imageset/time.png new file mode 100644 index 0000000..c031798 Binary files /dev/null and b/IOS_study/Assets.xcassets/jobIcons/time.imageset/time.png differ diff --git a/IOS_study/Assets.xcassets/jobIcons/want.imageset/Contents.json b/IOS_study/Assets.xcassets/jobIcons/want.imageset/Contents.json new file mode 100644 index 0000000..4216159 --- /dev/null +++ b/IOS_study/Assets.xcassets/jobIcons/want.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "want.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/jobIcons/want.imageset/want.png b/IOS_study/Assets.xcassets/jobIcons/want.imageset/want.png new file mode 100644 index 0000000..8d15708 Binary files /dev/null and b/IOS_study/Assets.xcassets/jobIcons/want.imageset/want.png differ diff --git a/IOS_study/Assets.xcassets/view.imageset/Contents.json b/IOS_study/Assets.xcassets/view.imageset/Contents.json new file mode 100644 index 0000000..780206c --- /dev/null +++ b/IOS_study/Assets.xcassets/view.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "冰岛极光风景4k壁纸3840x2160_彼岸图网.jpg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Assets.xcassets/view.imageset/冰岛极光风景4k壁纸3840x2160_彼岸图网.jpg b/IOS_study/Assets.xcassets/view.imageset/冰岛极光风景4k壁纸3840x2160_彼岸图网.jpg new file mode 100644 index 0000000..4cce52b Binary files /dev/null and b/IOS_study/Assets.xcassets/view.imageset/冰岛极光风景4k壁纸3840x2160_彼岸图网.jpg differ diff --git a/IOS_study/Auth/AgreeView.swift b/IOS_study/Auth/AgreeView.swift new file mode 100644 index 0000000..0765bcb --- /dev/null +++ b/IOS_study/Auth/AgreeView.swift @@ -0,0 +1,137 @@ +// +// AgreeView.swift +// IOS_study +// +// Created by CC-star on 2025/7/13. +// + +import SwiftUI + +struct AgreeView: View { + @Environment(\.dismiss) var dismiss + @Binding var agreeType: Int + var body: some View { + ScrollView { + VStack(spacing: 18) { + VStack { + Button { dismiss() } label: { Image(systemName: "xmark") }.push(to: .trailing).font(.title3).secondary() + Text(agreeType == 0 ? "用户协议" : "隐私政策").size18b() + } + Text(agreeType == 0 ? userAgreement : privacyPolicy).size15().tc2().lineSpacing(6) + }.padding() + }.bg() + } + + let userAgreement = """ + 一、用户协议总则 + 1、本协议双方为 Tian Xiao (以下简称"运营方")和 Boss_tdcat(包括手机APP等产品提供的服务,以下简称"产品和服务")的注册用户(以下简称"用户")。 + 2、用户在注册前请仔细阅读本协议的条款,并按照页面上的提示完成全部注册程序。 + 3、用户在进行注册过程中点击“同意”按钮,即表示用户已充分知悉和完全接受本协议项下全部条款,进而与运营方达成本协议。 + 4、运营方有权不时对本协议项下相关规则作出修改或补充,并于网站公布。用户若继续使用即视为您接受修订后的本协议条款。 + 二、用户服务使用说明 + 1、用户在注册时应按照注册提示填写准确的 手机号码 等相关个人资料,符合完整、准确、真实的要求。 + 2、用户一旦注册成功,便成为运营方合法的注册用户。用户应对其账号安全负全部责任,并应对其用户名下所进行的所有行为和事件承担相应的法律责任。 + 3、用户同意接受运营方通过或其他方式向用户发送有关商业信息。 + 4、运营方不对用户所发布信息的删除或储存失败负责。 + 5、运营方有判定用户的行为是否符合本网站服务条款要求的权利,如果用户违背了服务条款的规定,本网站有权对其用户所提供的网络服务进行中断或停止使用。 + 6、运营方不提供账号删除服务,如果用户需要删除账号,请直接放弃使用即可。 + 三、协议内容的变更和修订 + 1、运营方有权在必要时修改服务条款,修改后的协议可以在运营方上查看。 + 2、用户如果不同意运营方所改动的内容,可自行停止使用本站网络服务。 + 3、如果用户继续享用本站网络服务,则视为同意接受本网站服务条款的变动。 + 4、运营方可随时根据实际情况中断或终止一项或多项网络服务而无需对任何用户或第三方承担任何责任,如用户对一项或多项网络服务的中断或终止有异议,可以行使如下权利: + (1)自行停止使用运营方的网络服务。 + (2)通知运营方停止对该用户的服务。 结束用户服务后,用户使用网络服务的权利立即终止,从终止时起,用户没有权利再进行处理任何未完成的信息或服务,运营方也没有义务为其传送任何未处理的信息或未完成的服务给用户或任何第三方。 + 四、用户隐私保护 + 运营方将严格履行用户个人隐私保密义务,承诺不公开、编辑或透露用户个人信息,但以下特殊情况除外: + 1、经注册用户事先许可授权; + 2、遵守国家法律法规或配合相关政府部门的要求; + 3、遵从运营方合法服务程序; + 4、为维护社会公众利益以及运营方的合法权益所必须。 + 五、注册用户的权利与义务 + 1、注册用户在使用运营方产品和服务时,必须遵守中华人民共和国相关法律法规的规定,用户应同意将不会利用本服务进行任何违法或不正当的活动,否则用户将自行承担由此产生的一切法律责任。 + 2、用户在账号使用过程中不得上载、展示、张贴、传播或以其它方式传送含有下列内容之一的信息: + (1)危害国家安全,泄露国家秘密,颠覆国家政权,破坏国家统一的; + (2)损害国家荣誉和利益的; + (3)煽动民族仇恨、民族歧视、破坏民族团结的; + (4)破坏国家宗教政策,宣扬邪教和封建迷信的; + (5)散布谣言,扰乱社会秩序,破坏社会稳定的; + (6)散布淫秽、色情、赌博、暴力、凶杀、恐怖或者教唆犯罪的; + (7)侮辱或者诽谤他人,侵害他人合法权利的; + (8)含有虚假、有害、胁迫、侵害他人隐私、骚扰、侵害、中伤、粗俗、猥亵、或其它道德上令人反感的内容。 + 3、不得为任何非法目的而使用网络服务系统。 + 4、不得利用运营方产品和服务故意制作、传播计算机病毒等破坏性程序,或其他从事任何危害计算机信息网络安全的行为。 + 5、若用户行为违反上述约定,运营方有权作出独立判断并立即取消用户的服务账号,用户应对自己网上的行为承担一切法律责任,运营方的系统记录有可能作为用户违反法律的证据提交给相关主管部门。 + 6、用户应同意保障和维护运营方全体成员及其他用户的利益,如因违反本协议或违反有关的法律法规而给运营方或任何第三者造成损失,用户应承担因此产生的法律责任。 + 六、运营方网络服务内容的所有权 + 1、运营方定义的网络服务内容包括但不限于:招聘信息等。该等内容均受《著作权法》、《商标法》、《专利法》、《计算机软件保护条例》及其他相关法律法规的保护。 + 七、免责声明 + 1、用户同意承担使用运营方产品和服务所存在的一切风险以及因使用网络服务而产生的一切后果,运营方对用户不承担任何责任。 + 2、运营方不担保服务一定能满足用户的要求,也不担保服务不会中断,亦对服务的及时性,安全性及可能发生的技术错误均不作任何担保。 + 3、任何由于黑客攻击、计算机病毒侵入或发作、政府管制、硬件故障、不可抗力等非运营方故意或严重过失而造成的用户个人资料泄露、丢失、被盗用、被篡改或服务暂定或终止的,对用户可能造成的风险或损失,运营方不承担法律责任。 + 八、其他约定 + 1、用户同意因本平台服务产生的任何争议均适用中华人民共和国法律,相关争议任何一方可向运营方住所地人民法院提起诉讼解决。 + 2、本协议中的标题仅为方便而设,不影响对于条款本身的解释。本协议中的任何条款无论因何种原因完全或部分无效或不具有执行力,其余条款仍应具有约束力。 + 九、联系我们 + 如果您对本协议或本服务有任何疑问、意见或建议,可通过以下方式与我们联系: + (1)拨打客服热线电话13123456789 + (2)发送电子邮件至:mail@tdcat.cn + (3)联系地址:797687,联系人:Tian Xiao + (完) + """ + let privacyPolicy = """ + 欢迎您访问我们的产品。 Boss_tdcat (包括手机APP等产品提供的服务,以下简称“产品和服务”)是由 Tian Xiao (以下简称“我们”)开发并运营的。 确保用户的数据安全和隐私保护是我们的首要任务, 本隐私政策载明了您访问和使用我们的产品和服务时所收集的数据及其处理方式。 + 请您在继续使用我们的产品前务必认真仔细阅读并确认充分理解本隐私政策全部规则和要点, 一旦您选择使用,即视为您同意本隐私政策的全部内容,同意我们按其收集和使用您的相关信息。 如您在在阅读过程中,对本政策有任何疑问,可联系我们的客服咨询, 请通过 mail@tdcat.cn 或产品中的反馈方式与我们取得联系。 如您不同意相关协议或其中的任何条款的,您应停止使用我们的产品和服务。 + 本隐私政策帮助您了解以下内容: + 一、定义; + 二、我们如何收集和使用您的个人信息; + 三、我们如何存储和保护您的个人信息; + 四、我们如何共享、转让、公开披露您的个人信息; + 五、本政策如何更新; + 六、如何联系我们; + 一、定义 + 我们:指Tian Xiao。 + 个人信息:指以电子或者其他方式记录的与已识别或者可识别的自然人有关的各种信息,不包括匿名化处理后的信息。 + 个人敏感信息:指一旦泄露或者非法使用,可能导致个人受到歧视或者人身、财产受到严重危害的个人信息,包括种族、民族、宗教信仰、个人生物特征、医疗健康、金融账户、个人行踪等信息(我们将在本隐私政策中对具体个人敏感信息以粗体进行显著标识)。 + 未成年人:指不满18周岁的自然人。 + 儿童:指不满14周岁的自然人。 + 二、我们如何收集和使用您的个人信息 + 个人信息是指以电子或者其他方式记录的能够单独或者与其他信息, 结合识别特定自然人身份或者反映特定自然人活动情况的各种信息。 我们根据《中华人民共和国网络安全法》和《信息安全技术个人信息安全规范》(GB/T 35273-2017) 以及其它相关法律法规的要求,并严格遵循正当、合法、必要的原则, 出于您使用我们提供的服务和/或产品等过程中而收集和使用您的个人信息,包括但不限于电话号码等。 + 为接受我们全面的产品服务,您应首先注册一个用户账号,我们将通过它记录相关的数据。您所提供的所有信息均来自于您本人在注册时提供的数据。您准备使用的账户名、密码、您本人的联系方式, 我们可能通过发短信或者邮件的方式来验证您的身份是否有效。 + 三、我们如何存储和保护您的个人信息 + 作为一般规则,我们仅在实现信息收集目的所需的时间内保留您的个人信息。 我们会在对于管理与您之间的关系严格必要的时间内保留您的个人信息 (例如,当您开立帐户,从我们的产品获取服务时)。 出于遵守法律义务或为证明某项权利或合同满足适用的诉讼时效要求的目的, 我们可能需要在上述期限到期后保留您存档的个人信息,并且无法按您的要求删除。 如果您确认不再使用我们的产品和服务,并按照要求主动注销了您的账户,所有信息将被完全删除。 + 我们使用符合业界标准的安全防护措施保护您提供的个人信息,并加密其中的关键数据, 防止其遭到未经授权访问、公开披露、使用、修改、损坏或丢失。我们会采取一切合理可行的措施,保护您的个人信息。 我们会使用加密技术确保数据的保密性;我们会使用受信赖的保护机制防止数据遭到恶意攻击。 + 四、我们如何共享、转让、公开披露您的个人信息 + 在管理我们的日常业务活动所需要时,为追求合法利益以更好地服务客户, 我们将合规且恰当的使用您的个人信息。出于对业务和各个方面的综合考虑,我们仅自身使用这些数据,不与任何第三方分享。 + 我们可能会根据法律法规规定,或按政府主管部门的强制性要求,对外共享您的个人信息。 在符合法律法规的前提下,当我们收到上述披露信息的请求时,我们会要求必须出具与之相应的法律文件,如传票或调查函。 我们坚信,对于要求我们提供的信息,应该在法律允许的范围内尽可能保持透明。 + 在以下情形中,共享、转让、公开披露您的个人信息无需事先征得您的授权同意: + 1、与国家安全、国防安全直接相关的; + 2、与犯罪侦查、起诉、审判和判决执行等直接相关的; + 3、出于维护您或其他个人的生命、财产等重大合法权益但又很难得到本人同意的; + 4、您自行向社会公众公开的个人信息; + 5、从合法公开披露的信息中收集个人信息的,如合法的新闻报道、政府信息公开等渠道。 + 6、根据个人信息主体要求签订和履行合同所必需的; + 7、用于维护所提供的产品或服务的安全稳定运行所必需的,例如发现、处置产品或服务的故障; + 8、法律法规规定的其他情形。 + 五、本政策如何更新 + 我们的隐私政策可能变更。 + 未经您明确同意,我们不会削减您在本政策项下应享有的权利。我们会在本页面上发布对本政策所作的任何变更。 + 对于重大变更,我们还会提供更为显著的通知(比如官方公告、推送通知、发送短信或者电子邮件),并在本政策更新后您首次登陆时再次获取您的明示同意。若您在本政策更新且接到我们的通知后,点击同意或下一步,这表示您已充分阅读、理解并接受修订后的本政策。 + 本政策所指的重大变更包括但不限于: + 1、我们的服务模式发生重大变化,比如处理个人信息的类型、目的和方式等; + 2、我们在所有权结构、组织架构等方面发生重大变化,比如业务调整、并购、破产等引起的所有者变更等; + 3、个人信息共享、转让或者公开披露的主要对象发生变化; + 4、您参与个人信息处理方面的权利及其行使方式发生重大变化; + 5、我们负责处理个人信息安全的责任部门、联络方式及投诉渠道发生变化时; + 6、个人信息安全影响评估报告表明存在高风险时。 + 我们还会将本政策的旧版本存档,供您查阅。 + 六、如何联系我们 + 1.如果您对本政策有任何疑问、意见或建议,可通过以下方式与我们联系: + (1)拨打客服热线电话13123456789 + (2)发送电子邮件至:mail@tdcat.cn + (3)联系地址:797687,联系人:Tian Xiao + 我们的客服部门将会同个人信息保护部门30天内进行回复,并协助解决您的问题。 + 2.如果您对我们的回复不满意,特别是我们的个人信息处理行为损害了您的合法权益,您还可以通过以下外部途径寻求解决方式:向Tian Xiao所在地人民法院提起诉讼,或向网信、工商、公安等监管部门进行投诉或举报。 + """ + +} diff --git a/IOS_study/Auth/AuthViewModel.swift b/IOS_study/Auth/AuthViewModel.swift new file mode 100644 index 0000000..7f58267 --- /dev/null +++ b/IOS_study/Auth/AuthViewModel.swift @@ -0,0 +1,29 @@ +// +// AuthViewModel.swift +// IOS_study +// +// Created by CC-star on 2025/7/15. +// + +import Foundation +import LeanCloud + +@Observable final class AuthViewModel { + var isLogin = LCApplication.default.currentUser != nil + + init() { + NotificationCenter.default.addObserver(self, selector: #selector(handleLogin), name: .loginSuccess, object: nil)//监听一条通知,nil表示监听所有的通知 + NotificationCenter.default.addObserver(self, selector: #selector(handlelogout), name: .logoutSuccess, object: nil) + } + + @objc func handleLogin() { + isLogin = true + } + @objc func handlelogout() { + isLogin = false + } + + deinit {//销毁当前对象中的所有监听 + NotificationCenter.default.removeObserver(self) + } +} diff --git a/IOS_study/Auth/SignInPhoneView+.swift b/IOS_study/Auth/SignInPhoneView+.swift new file mode 100644 index 0000000..d6af210 --- /dev/null +++ b/IOS_study/Auth/SignInPhoneView+.swift @@ -0,0 +1,45 @@ +// +// SignInPhoneView+.swift +// IOS_study +// +// Created by CC-star on 2025/7/10. +// + +import Foundation + +extension SignInPhoneView { + func stopTimer() { timer?.upstream.connect().cancel(); timer = nil }//停止定时器 + func getVerCode() async {//async -> 异步任务 + guard vm.phoneNum.isPhoneNum else { return } + do { + //todo 签名和模版要修改为审核通过的名字 + try await vm.getVerCode(phoneNum: "+86\(vm.phoneNum)", tName: "login", sName: "名称") + } catch { + print("获取验证码失败:\(error)") + hud.show("获取验证码失败:\(error)") + } + } + + func preLogin() { + focusedField = nil + if isAgree { + Task { await login() } + } else { + showAgreeSheet = true + } + } + //测试手机号:+8613123456789 验证码:797687 + func login() async { + guard vm.phoneNum.isPhoneNum, vm.verCode.isVerCode else { hud.show("手机号或验证码错误"); return } + vm.isLogining = true + defer { vm.isLogining = false } + do { + try await vm.login() + stopTimer() + hud.show("登录成功") + } catch { + print("登录失败:\(error)") + hud.show("登录失败:\(error)") + } + } +} diff --git a/IOS_study/Auth/SignInPhoneView.swift b/IOS_study/Auth/SignInPhoneView.swift new file mode 100644 index 0000000..81f2416 --- /dev/null +++ b/IOS_study/Auth/SignInPhoneView.swift @@ -0,0 +1,149 @@ +// +// SignInPhoneView.swift +// IOS_study +// +// Created by CC-star on 2025/7/10. +// + +import SwiftUI +import Combine +private let totalTime = 60 + +struct SignInPhoneView: View { + enum FocusedField { case phoneNum, verCode }//聚焦在那个case + @Environment(SignInPhoneViewModel.self) var vm + @Environment(HUD.self) var hud + @State var showVerBtn = true + @State var timer: Publishers.Autoconnect?//后面的 ? 表示可选类型 + @State var timeRemain = totalTime//倒计时 + @State var isAgree = false //是否同意协议,默认未勾选 + @State var showAgreeSheet = false//确认协议弹窗 + @State var showAgreeView = false//展示协议 + @State var agreeType = 0//0表示用户协议,1表示隐私政策 + @FocusState var focusedField: FocusedField?//光标聚焦用,设置为可选性,方便赋予nil + var enableLoginBtn: Bool { vm.phoneNum.isPhoneNum && vm.verCode.isVerCode } + + var totalNumCount: Int { vm.phoneNum.count + vm.verCode.count } + + var body: some View { + @Bindable var vm = vm + NavigationStack { + ScrollView { + VStack(spacing: 33) { + HStack { + NavigationLink { + SettginsView(naviPath: .constant(NavigationPath()), isLogin: false) + } label: { + Image(systemName: "gearshape") + } + Button { + focusedField = nil//不聚焦光标 -> 收起键盘 + } label: { + Image(systemName: "xmark") + } + }.push(to: .trailing).font(.title3).secondary() + Text("手机号登陆").tc().font(.title).bold() + .kerning(1.2)//字间距,默认1 + + VStack(spacing: 14) { + HStack { + TextField("请输入手机号", text: $vm.phoneNum).tc().kerning(1.2) + .keyboardType(.numberPad)//数字键盘 + .focused($focusedField, equals: .phoneNum) + if !vm.phoneNum.isEmpty { + Image(systemName: "xmark.circle.fill").font(.title2).secondary().onTapGesture { vm.phoneNum = "" } + } + } + exDivider() + + HStack { + //797687 + TextField("请输入验证码", text: $vm.verCode).tc().tc().kerning(1.2).keyboardType(.numberPad) + .focused($focusedField, equals: .verCode) + if vm.phoneNum.isPhoneNum || !showVerBtn {//是否为中国的手机号 + if showVerBtn { + Button("获取验证码") { + timeRemain = totalTime + focusedField = .verCode//光标聚焦在case为verCode的区域 + timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()//每隔一秒,在主循环,自动执行 + showVerBtn = false + Task { + await getVerCode() + } + }.headline() + + } else { + if let timer { + Text("重新发送(\(timeRemain)s)").headline().secondary() + //监听异步任务 + .onReceive(timer) { _ in + timeRemain -= 1 + if timeRemain <= 0 { + stopTimer() + showVerBtn = true + } + } + } + } + } + } + exDivider() + Button { + preLogin() + } label: { + if vm.isLogining { + ProgressView().appleStyleP() + } else { + Text("登录").appleStyle(disabled: !enableLoginBtn) + } + }.disabled(!enableLoginBtn || vm.isLogining) + HStack(spacing: 3) { + Button { isAgree.toggle()} label: { + Image(systemName: isAgree ? "checkmark.circle.fill" : "circle") + }.tint(isAgree ? .accent : .secondary).size16() + agreeTextView.size14() + } + }.font(.title3) + }.padding(33) + } + .scrollDismissesKeyboard(.immediately).bg()//滚动时立刻收起键盘 + .fullScreenCover(isPresented: $showAgreeView, content: { AgreeView(agreeType: $agreeType) }) + .sheet(isPresented: $showAgreeSheet) {//弹窗 + VStack(spacing: 18) { + agreeTextView.size15() + Button { + isAgree = true//同意全部协议 + showAgreeSheet = false//关闭弹窗 + Task { await login() }//登录 + } label: { + Text("同意并登录").font(.body).push(to: .center).padding(.vertical, 6) + }.buttonStyle(.borderedProminent) + }.padding(33) + .presentationDetents([.height(200)])//指定弹窗展示多少 + // .presentationDetents([.medium, .large])//可以拖动弹窗展示多少 + } + } + .onChange(of: totalNumCount) { + if totalNumCount == 17 { + preLogin() + } + + + } + } + + var agreeTextView: some View { + HStack(spacing: 3) { + Text("我已阅读并同意").secondary() + Button("用户协议") { + agreeType = 0 + showAgreeView = true + } + Text("和").secondary() + Button("隐私政策") { + agreeType = 1 + showAgreeView = true + } + } + } +} diff --git a/IOS_study/Auth/SignInPhoneViewModel.swift b/IOS_study/Auth/SignInPhoneViewModel.swift new file mode 100644 index 0000000..e391b05 --- /dev/null +++ b/IOS_study/Auth/SignInPhoneViewModel.swift @@ -0,0 +1,37 @@ +// +// SignInPhoneViewModel.swift +// IOS_study +// +// Created by CC-star on 2025/7/10. +// + +import Foundation + +@Observable final class SignInPhoneViewModel { +// @Environment(AuthViewModel.self) var vm + var phoneNum = "" + var verCode = "" + var isLogining = false + + + @MainActor func getVerCode(phoneNum: String, tName: String, sName: String) async throws { + try await AuthManager.shared.getVerCode(phoneNum: phoneNum, tName: tName, sName: sName) + } + + @MainActor func login() async throws { + let user = try await AuthManager.shared.login(phoneNum: phoneNum, verCode: verCode) + // printf(user.mobilePhoneNumber?.value) + + if let isNewUser = user.get("isNewUser")?.boolValue, !isNewUser {//不是首次登录 + + } else {//首次登录 + try user.set("isNewUser", value: false) + try await user.save()//保存信息 + + try await UserManager.shared.create(user: user) + } + NotificationCenter.default.post(name: .loginSuccess, object: nil)//发送一条通知 + verCode = ""//清空验证码 + } + +} diff --git a/IOS_study/Constant/CJob.swift b/IOS_study/Constant/CJob.swift new file mode 100644 index 0000000..d0ce300 --- /dev/null +++ b/IOS_study/Constant/CJob.swift @@ -0,0 +1,43 @@ +// +// CJob.swift +// 项目常量 +// +// Created by CC-star on 2025/6/28. +// + +import Foundation + +let kJobLimitS = 2 +let kJobLimit = 10 +let kTFAndPHeight: CGFloat = 36 + + +let kBusinessArr = [ "互联网", "便利店", "饭馆", "制造业", "金融", "教育", "医疗健康", "零售", "房地产", "物流", "文化传媒", "咨询服务", "政府机关", "公共事业", "新能源", "旅游", "电子商务", "汽车", "餐饮", "IT服务" ] +let kCityArr = [ + "北京", "上海", "广州", "深圳", "杭州", + "南京", "武汉", "成都", "西安", "重庆", + "天津", "苏州", "郑州", "长沙", "青岛", + "大连", "宁波", "厦门", "无锡", "福州" +] + +let kTaxArr = ["详谈","报税","不报税"] +let kWantNumArr = [1,2,3,4,5,6,7,8,9,10,13,15,16,63,75,856,345] +let kNeedExpArr = ["经验不限","1年以内","1-3年","3-5年","5-10年"] +let kNeedEduArr = ["中专","高中","大专","本科","硕士研究生","博士研究生","博士后"] +let kNeedLanArr = ["Swift","Objective-c","Java","C","Vue","Golong","Kotlin"] +let kNeedFrameArr = ["SwiftUI","UIKit","Spring","Jetpack Compose"] +let kProvinceArr = [ + "北京", "天津", "上海", "重庆", // 直辖市 + "河北", "山西", "辽宁", "吉林", "黑龙江", // 华北、东北 + "江苏", "浙江", "安徽", "福建", "江西", "山东", // 华东 + "河南", "湖北", "湖南", // 华中 + "广东", "海南", // 华南 + "四川", "贵州", "云南", // 西南 + "陕西", "甘肃", "青海", // 西北 + "台湾", // 台湾省 + "内蒙古", "广西", "西藏", "宁夏", "新疆", // 自治区 + "香港", "澳门" // 特别行政区 +] +let kContactTypeArr = ["微信","QQ","电话","邮件","其他"] + +let jobReportsKey = "jobReports"//内存中的举报key diff --git a/IOS_study/Constant/CUser.swift b/IOS_study/Constant/CUser.swift new file mode 100644 index 0000000..5612f5f --- /dev/null +++ b/IOS_study/Constant/CUser.swift @@ -0,0 +1,21 @@ +// +// CUser.swift +// 基本常量定义 +// +// Created by CC-star on 2025/6/14. +// + +import Foundation + +let kAppName = "数据猫" +let kIconSpacing: CGFloat = 3 +let kStackSpacing: CGFloat = 14 +let kStackSpacingS: CGFloat = 9 + +let KdevContact = "IC_lucky"//客服微信 + +//LeanCloud +let kAppID = "jHdEBD3pF6YLyDlJQXEn8bK1-gzGzoHsz" +let kAppKey = "48dkfFMv5oa0pvtORJtkEZyM" +let kServerURLStr = "https://boss.tdcat.cn" + diff --git a/IOS_study/Constant/GFunc.swift b/IOS_study/Constant/GFunc.swift new file mode 100644 index 0000000..2d90db7 --- /dev/null +++ b/IOS_study/Constant/GFunc.swift @@ -0,0 +1,11 @@ +// +// GFunc.swift +// IOS_study +// +// Created by CC-star on 2025/7/3. +// + +import Foundation + +func sleep5() async throws { try await Task.sleep(nanoseconds: 5_000_000_000) }//延迟5s +func sleep3() async throws { try await Task.sleep(nanoseconds: 3_000_000_000) } diff --git a/IOS_study/Constant/GType.swift b/IOS_study/Constant/GType.swift new file mode 100644 index 0000000..67cbbe2 --- /dev/null +++ b/IOS_study/Constant/GType.swift @@ -0,0 +1,19 @@ +// +// GType.swift +// 导航相关 +// +// Created by CC-star on 2025/6/30. +// + +import Foundation + +enum Navi1 { + case PLJobsToPLJob +} + +enum Navi5 { + case AddCompany,EditCompany + case PLJobsToPLJob + case PLJobToEdit + case Tosetting +} diff --git a/IOS_study/Constant/GView.swift b/IOS_study/Constant/GView.swift new file mode 100644 index 0000000..e9fd6c2 --- /dev/null +++ b/IOS_study/Constant/GView.swift @@ -0,0 +1,35 @@ +// +// GView.swift +// IOS_study +// +// Created by CC-star on 2025/6/26. +// + +import SwiftUI + +func exDivider() -> some View{ + Rectangle().fill(Color.labeBG).frame(height: 1)//分割线[系统自带的Divider有时候不显示] +} + + +//筛选 +func arrowIcon(_ showView: Bool) -> some View{ + showView ? Image(systemName: "arrowtriangle.up.fill").size9() : Image(systemName: "arrowtriangle.down.fill").size9() +} + +func listBottomEView() -> some View{ + Color.clear.frame(height: 120).listRowStyle() +} + +func postBottomEView() -> some View{ + Color.clear.frame(height: 120) +} + +func BprogressView(text: String = "加载中") -> some View { + VStack { + ProgressView() + Text(text).font(.subheadline).secondary() + }.padding(25) + .background(.thinMaterial)//毛玻璃效果 + .radius(6) +} diff --git a/IOS_study/Extension/Foundation+.swift b/IOS_study/Extension/Foundation+.swift new file mode 100644 index 0000000..f301658 --- /dev/null +++ b/IOS_study/Extension/Foundation+.swift @@ -0,0 +1,94 @@ +// +// Foundation+.swift +// 常规扩展 +// +// Created by CC-star on 2025/6/29. +// + +import Foundation + + +//以1开头,再加10个数字【\d】(\\d -> 转义字符\ -> \d),并以数字结尾 +let kPhoneRegEx = "^1\\d{10}$"//中国手机号正则表达式 + +let kVerCodeRegEx = "^\\d{6}$"//验证码正则表达式 +extension NSRegularExpression { + convenience init(_ pattern: String) { + do { + try self.init(pattern: pattern) + } catch { + fatalError("非法的正则表达式")//因不能确保调用父类的init函数 + } + } + func matches(_ string: String) -> Bool { + let range = NSRange(location: 0, length: string.utf16.count) + return firstMatch(in: string, options: [], range: range) != nil + } +} + +extension String { + var isBlank: Bool { self.isEmpty || self.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }//没有输入或者输入空格 + var trimmed: String { self.trimmingCharacters(in: .whitespacesAndNewlines) } + var isPhoneNum: Bool { + Int(self) != nil && NSRegularExpression(kPhoneRegEx).matches(self) + //判断该字符串是否可以转化为int类型 nor 置空 -> 判断用户是否输入的是数字 + } + var isVerCode: Bool { Int(self) != nil && NSRegularExpression(kVerCodeRegEx).matches(self) } + //随机字母和数字组合 + static func randomString(_ length: Int) -> String{ + let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + return String((0.. LCObject { + try await withCheckedThrowingContinuation { continuation in + _ = self.getFirst { result in + switch result { + case .success(object: let lcObject): + continuation.resume(returning: lcObject) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + /// 异步获取匹配条件的对象数量 + func count() async throws -> Int { + try await withCheckedThrowingContinuation { continuation in + self.count { result in + switch result { + case .success(let count): + continuation.resume(returning: count) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + //返回lcobject数组 + func find() async throws -> [LCObject] { + try await withCheckedThrowingContinuation { continuation in + self.find { result in + switch result { + case .success(let objects): + continuation.resume(returning: objects) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + //此方法不用,仅供学习 + func find(as type: T.Type) async throws -> [T] { + let lcObjects = try await withCheckedThrowingContinuation { continuation in + self.find { result in + switch result { + case .success(objects: let lcObjects): + continuation.resume(returning: lcObjects) + case .failure(error: let error): + continuation.resume(throwing: error) + } + } + } + var res: [T] = [] + for lcObject in lcObjects { + //dictionaryValue失效,无法转化成字典类型 + guard let lcDic = lcObject.dictionaryValue else { throw UserError.lcObjToDicError } + let data = try JSONSerialization.data(withJSONObject: lcDic) + let object = try JSONDecoder().decode(T.self, from: data) + res.append(object) + } + return res + } +} + + +extension LCObject { + //将 LeanCloud 的保存方法操作保存包转为 async 方法 + func save() async throws { + try await withCheckedThrowingContinuation { continuation in + _ = self.save() { result in + switch result { + case .success: + //保存成功 + continuation.resume() + case .failure(let error): + //保存失败,抛出错误 + continuation.resume(throwing: error) + } + } + } + } + + /// 异步删除方法 + func delete() async throws { + try await withCheckedThrowingContinuation { continuation in + _ = self.delete { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + /// 批量删除对象的异步方法 + static func delete(_ objects: [LCObject]) async throws { + try await withCheckedThrowingContinuation { continuation in + self.delete(objects) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + +} + +extension LCSMSClient { + //把requestShortMessage(请求验证码)扩展成async方法 + static func requestShortMessage( + mobilePhoneNumber: String, + templateName: String, + signatureName: String + ) async throws { + try await withCheckedThrowingContinuation { continuation in + LCSMSClient.requestShortMessage( + mobilePhoneNumber: mobilePhoneNumber, + templateName: templateName, + signatureName: signatureName +// ,variables: ["name": LCString("BOSS大直招"), "ttl": LCNumber("5")] + ) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } +} + +extension LCUser { + //把signUpOrLogIn(验证码注册或登录)扩展成async方法 + static func signUpOrLogIn(mobilePhoneNumber: String, verificationCode: String) async throws -> LCUser { + try await withCheckedThrowingContinuation { continuation in + _ = LCUser.signUpOrLogIn(mobilePhoneNumber: mobilePhoneNumber, verificationCode: verificationCode) { result in + switch result { + case .success(object: let user): + continuation.resume(returning: user) + case .failure(error: let error): + continuation.resume(throwing: error) + } + } + } + } +} + + +extension LCFile { + func save() async throws -> String { + try await withCheckedThrowingContinuation { continuation in + _ = self.save { result in + switch result { + case .success: + if let url = self.url?.value { + continuation.resume(returning: url) // 保存成功,返回文件 URL + } else { + let error = UserError.saveFileSuccessButNoURL // 自定义错误 + continuation.resume(throwing: error) + } + case .failure(let error): + continuation.resume(throwing: error) // 直接抛出 LeanCloud 的错误 + } + } + } + } +} diff --git a/IOS_study/Extension/View+.swift b/IOS_study/Extension/View+.swift new file mode 100644 index 0000000..92df117 --- /dev/null +++ b/IOS_study/Extension/View+.swift @@ -0,0 +1,99 @@ +// +// View+.swift +// 样式扩展 +// +// Created by CC-star on 2025/6/12. +// + +import SwiftUI + +extension View{ + + //矩形圆角 + func radius(_ radius: CGFloat = 4) -> some View{ clipShape(RoundedRectangle(cornerRadius: radius)) } +// func radius2(_ radius: CGFloat = 4) -> some View{ clipShape(RoundedRectangle(cornerRadius: radius)) } + func border(radius: CGFloat = 4, borderColor: Color) -> some View{ + overlay { RoundedRectangle(cornerRadius: radius).strokeBorder(borderColor, lineWidth: 1) } + } + + //颜色 + func tc() -> some View{ foregroundStyle(.aTC) } + func tc2() -> some View{ foregroundStyle(.aTC2) } + func secondary() -> some View{ foregroundStyle(.secondary) } + func accent() -> some View{ foregroundStyle(.accent) } + func white() -> some View{ foregroundStyle(.white) } + func red() -> some View{ foregroundStyle(.red) } + func gray() -> some View{ foregroundStyle(.gray) } + func purple() -> some View{ foregroundStyle(.purple) } + func bg() -> some View{ background(Color.aBG) } + func bgA() -> some View{ background(Color.accent) } + func bg2() -> some View{ background(Color.aBG2) } + + //字体大小 + func size9() -> some View{ font(.system(size: 9)) } + func size11() -> some View{ font(.system(size: 11)) } + func size12() -> some View{ font(.system(size: 12)) } + func size13() -> some View{ font(.system(size: 13)) } + func size13s() -> some View{ size13().secondary() } + func size13sb() -> some View{ size13().secondary().bold() } + func size14() -> some View{ font(.system(size: 14)) } + func size15semib() -> some View{ font(.system(size: 15)).semib() } + func size15() -> some View{ font(.system(size: 15)) } + func size15sb() -> some View{ font(.system(size: 15)).secondary().bold() } + func size15pb() -> some View{ font(.system(size: 15)).foregroundStyle(.pink).bold() } + func size16() -> some View{ font(.system(size: 16)) } + func size16semib() -> some View{ font(.system(size: 16)).semib() } + func size17() -> some View{ font(.system(size: 17)) } + func headline() -> some View{ font(.headline) } + func size18b() -> some View{ font(.system(size: 18)).bold() } + func size18ab() -> some View{ font(.system(size: 18)).bold().accent() } + func size20b() -> some View{ font(.title3).bold() } + + //字体粗细 + func med() -> some View{ fontWeight(.medium) } + func semib() -> some View{ fontWeight(.semibold) } + + func push(to alignment: Alignment) -> some View { + frame(maxWidth: .infinity, alignment: alignment) + } + + //List多行文本必备 + func allowMulti() -> some View{ + fixedSize(horizontal: false, vertical: true).textSelection(.enabled).multilineTextAlignment(.leading) + } + //图片相关 + func thumbFrame() -> some View { + frame(width: 100, height: 100) + } + func thumbFrameS(aspectRatio: CGFloat = 1) -> some View { + frame(width: 40, height: 40 / aspectRatio) + } + +} + +extension Color{ + static let labeBG = Color(.systemFill) + static let labelBG2 = Color(.systemGray5) + static let bg = Color.aBG + static let bg2 = Color.aBG2 +} + + +// MARK: - 让右滑返回上一页有效 +extension UINavigationController: @retroactive UIGestureRecognizerDelegate { + override open func viewDidLoad() { + super.viewDidLoad() + interactivePopGestureRecognizer?.delegate = self + } + + public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { return viewControllers.count > 1 } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { true } +} + +//收起键盘 +extension UIApplication { + func hideKeyboard() { + sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } +} diff --git a/IOS_study/Extension/View+Label.swift b/IOS_study/Extension/View+Label.swift new file mode 100644 index 0000000..c2561fc --- /dev/null +++ b/IOS_study/Extension/View+Label.swift @@ -0,0 +1,36 @@ +// +// View+Label.swift +// IOS_study +// +// Created by CC-star on 2025/6/12. +// + +import SwiftUI + +extension View { + func padding1() -> some View { padding(.vertical,2).padding(.horizontal,3) } + func padding2() -> some View { padding(.vertical,3).padding(.horizontal,7) } + func padding3() -> some View { padding(.vertical,4).padding(.horizontal,7) } + func redBorder() -> some View { size12().red().semib().padding1().border(borderColor: Color.red) } + func grayBorder() -> some View {size12().gray().semib().padding1().border(borderColor: Color.gray) } + + func accentBorder() -> some View{ size12().accent().semib().padding1().border(borderColor: Color.accent) } + + + func labelBG() -> some View{ size13().tc().semib().padding3() + .background(Color.labeBG)//背景一般先padding,再background + .radius() + } + + func labelBG2() -> some View{ size13().tc().med().padding3().background(Color.labelBG2).radius()} + func labelPP() -> some View{ size13().purple().med().padding3().background(Color.purple.opacity(0.18)).radius() } + func labelWA() -> some View{ size11().white().padding1().background(Color.accent).radius() } + func labelWAL() -> some View{ size14().white().med().padding3().background(Color.accent).radius() } + + //个人页的一些Label + func labelPL() -> some View{ font(.caption).foregroundStyle(.blue).padding2().background(.blue.opacity(0.18)).radius() } + //主题色背景 - 已下线、好评 + func labelTA() -> some View{ size13().tc2().med().padding3().background(.accent.opacity(0.18)).radius() } + + +} diff --git a/IOS_study/Extension/View+Style.swift b/IOS_study/Extension/View+Style.swift new file mode 100644 index 0000000..d7a66c6 --- /dev/null +++ b/IOS_study/Extension/View+Style.swift @@ -0,0 +1,136 @@ +// +// View+Style.swift +// style扩展 +// +// Created by CC-star on 2025/6/12. +// + +import SwiftUI + +extension View{ + + func btnStyle() -> some View{ size15().buttonStyle(.borderless).accent() } + + func textStyle() -> some View{ size15().tc().lineSpacing(6) } + func textStyle2() -> some View{ size15().tc2().lineSpacing(6) } + func oneLineStyleMiddle() -> some View{ lineLimit(1).truncationMode(.middle) } + func explainStyle() -> some View { size15sb().frame(maxWidth: .infinity, maxHeight: .infinity).padding() }//问题说明性提示文字 + //navi + func naviBarStyle() -> some View { + navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .toolbarBackground(.aBG, for: .navigationBar) } + + //筛选 + func labelStyleOF() -> some View { tc2().size15semib() } + func badgeStyle() -> some View { size13().semib().white().padding(6).bgA()//筛选的个数样式 + .clipShape(Circle())//clipShape裁剪成圆形 + } + + @ViewBuilder func selectBtnStyle(_ selected: Bool) -> some View {//ViewBuilder允许返回不同的视图 + if selected { + buttonStyle(.borderedProminent) + } else { + tint(Color.aTC).buttonStyle(.bordered) + } + } + + + //List里面的东西 + func subTStyle() -> some View{ + size16semib().tc() + } + + func subTStyleL() -> some View{ + headline().tc() + } + + + + //cell + func cellPaddingRadius() -> some View{//带radius + padding(.vertical) + .padding(.horizontal,12) + .bg() + .radius(8) + } + func cellOutPadding() -> some View{ + padding([.top,.horizontal],9) + //.padding(.bottom,10) + } + + //list + func listRowStyle() -> some View{ + listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + } + + func listStyle() -> some View{ + listStyle(.inset) + .scrollContentBackground(.hidden) + .bg2() + .environment(\.defaultMinListRowHeight,0) } + + func listRowStyleLoading2() -> some View {//加载更新数据视图 + padding(.top).padding(.bottom,100).size13sb().push(to: .center).bg2().listRowStyle() + } + + //总的 + func cellStyle() -> some View{ + cellPaddingRadius().cellOutPadding().listRowStyle() + } + + //Form里面的 + func littleTStyle() -> some View{ + size13().tc2().fontWeight(.light) + } + func pStyle() -> some View{ + frame(height: kTFAndPHeight).border(borderColor: Color(.systemGray4)) + } + func tfStyle() -> some View{//单行 + frame(height: kTFAndPHeight).padding(.horizontal,9).border(borderColor: Color(.systemGray4)).submitLabel(.done) + } + func tfStyleMultiS() -> some View{ + lineLimit(1...4).frame(minHeight: kTFAndPHeight).padding(.horizontal,9).border(borderColor: Color(.systemGray4)).lineSpacing(4) + .fixedSize(horizontal: false, vertical: true) + } + func tfStyleMultiM() -> some View{ + lineLimit(3...8).frame(minHeight: kTFAndPHeight).padding(.horizontal,9).border(borderColor: Color(.systemGray4)).lineSpacing(4) + .fixedSize(horizontal: false, vertical: true) + } + func tfStyleMultiL() -> some View{ + lineLimit(4...10).frame(minHeight: kTFAndPHeight).padding(.horizontal,9).border(borderColor: Color(.systemGray4)).lineSpacing(4) + .fixedSize(horizontal: false, vertical: true) + } + func conlonStyle() -> some View{ + bold().padding(.horizontal,2).offset(y: -1) + } + func toggleStyle(isTap: Bool) -> some View{ + size15().foregroundStyle(isTap ? .white : .aTC).padding(.horizontal,10).frame(height: kTFAndPHeight) + .background(isTap ? Color.accent.gradient : Color.bg.gradient).radius() + .border(borderColor: isTap ? .clear : .accent) + } + + func appleStyleS() -> some View{ font(.body).frame(maxWidth: .infinity).padding(.vertical, 6) } + func appleStyle(disabled: Bool = false) -> some View{ + headline().white().frame(height: 55).frame(maxWidth: .infinity) + .background(disabled ? Color.secondary.gradient : Color.accent.gradient).radius() } + func appleStyleP() -> some View{ tint(.white).frame(height: 55).frame(maxWidth: .infinity).background(.accent.gradient).radius() } + + + //个人页 + //文本 + func loadingJobStyle() -> some View { size13().white().frame(width: 150, height: 25).background(Color.accent.gradient).radius(6) } + //progress + func loadingJobStyleP() -> some View { tint(.white).frame(width: 150, height: 25).background(Color.accent.gradient).radius(6) } + func settingsStyle() -> some View { tc().size16() } +} + + +extension Image{ + func iconStyle() -> some View { + resizable().frame(width: 16, height: 16) + } +} + diff --git a/IOS_study/IOS_studyApp.swift b/IOS_study/IOS_studyApp.swift new file mode 100644 index 0000000..c1c3c34 --- /dev/null +++ b/IOS_study/IOS_studyApp.swift @@ -0,0 +1,59 @@ +// +// IOS_studyApp.swift +// IOS_study +// +// Created by CC-star on 2025/6/3. +// + +import SwiftUI +import LeanCloud + +@main +struct IOS_studyApp: App { + // init(){ + // + // } + @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate + @State var hud = HUD() + @State var tabbarVM = TabBarViewModel() + @State var signInPhoneVM = SignInPhoneViewModel() + @State var reviewVM = ReviewViewModel() + @State var ProfileVM = ProfileViewModel() + @State var nwMonitor = NetworkMonitor() + + var body: some Scene { + WindowGroup { + TaBarView() + // PostCompanyView(naviPath: .constant(NavigationPath())) + .hud(isPresented: $hud.isPresented){ + Text(hud.title) + } + //注入 + .environment(reviewVM) + .environment(ProfileVM) + .environment(signInPhoneVM) + .environment(hud) + .environment(tabbarVM) + .environment(nwMonitor) + } + } +} + +@MainActor class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + UISegmentedControl.appearance().selectedSegmentTintColor = .accent + UISegmentedControl.appearance().setTitleTextAttributes([.font: UIFont.systemFont(ofSize: 17,weight: .medium),.foregroundColor: UIColor(Color.aTC)], for: .normal) + UISegmentedControl.appearance().setTitleTextAttributes([.font: UIFont.systemFont(ofSize: 17,weight: .semibold),.foregroundColor: UIColor(Color.white)], for: .selected) + + + do { + try LCApplication.default.set( + id: kAppID, + key: kAppKey, + serverURL: kServerURLStr) + } catch { + print(error) + } + return true + } +} diff --git a/IOS_study/Img/Git/下线确定样式.webp b/IOS_study/Img/Git/下线确定样式.webp new file mode 100644 index 0000000..9dd6ad4 Binary files /dev/null and b/IOS_study/Img/Git/下线确定样式.webp differ diff --git a/IOS_study/Img/Git/发布公司.webp b/IOS_study/Img/Git/发布公司.webp new file mode 100644 index 0000000..c960468 Binary files /dev/null and b/IOS_study/Img/Git/发布公司.webp differ diff --git a/IOS_study/Img/Git/发布工作样式.webp b/IOS_study/Img/Git/发布工作样式.webp new file mode 100644 index 0000000..a866b13 Binary files /dev/null and b/IOS_study/Img/Git/发布工作样式.webp differ diff --git a/IOS_study/Img/Git/工作卡片样式.webp b/IOS_study/Img/Git/工作卡片样式.webp new file mode 100644 index 0000000..580018e Binary files /dev/null and b/IOS_study/Img/Git/工作卡片样式.webp differ diff --git a/IOS_study/Img/Git/工作详情样式.webp b/IOS_study/Img/Git/工作详情样式.webp new file mode 100644 index 0000000..1f75688 Binary files /dev/null and b/IOS_study/Img/Git/工作详情样式.webp differ diff --git a/IOS_study/Img/Git/弹窗提醒.webp b/IOS_study/Img/Git/弹窗提醒.webp new file mode 100644 index 0000000..3d81524 Binary files /dev/null and b/IOS_study/Img/Git/弹窗提醒.webp differ diff --git a/IOS_study/Img/Git/我的页面.webp b/IOS_study/Img/Git/我的页面.webp new file mode 100644 index 0000000..0a28a2d Binary files /dev/null and b/IOS_study/Img/Git/我的页面.webp differ diff --git a/IOS_study/Img/chongwutubiao04.png b/IOS_study/Img/chongwutubiao04.png new file mode 100644 index 0000000..1760c70 Binary files /dev/null and b/IOS_study/Img/chongwutubiao04.png differ diff --git a/IOS_study/Job/PLJobView+.swift b/IOS_study/Job/PLJobView+.swift new file mode 100644 index 0000000..80d8f8c --- /dev/null +++ b/IOS_study/Job/PLJobView+.swift @@ -0,0 +1,34 @@ +// +// PLJobView+.swift +// IOS_study +// +// Created by CC-star on 2025/7/1. +// + +import SwiftUI + +extension PLJobView { + func toggleJobStatus() async { + do { + try await vm.toggleJobStatus(id: job.id) + profileVM.shouldRefreshMyPLJobs = true + hud.show(isActive ? "上线成功" : "下线成功")//这里是相反的逻辑,应为之前已经反转过一次了 + } catch { + print("修改pljob的状态失败:\(error)") + hud.show("修改失败,请检查网络并重试") + vm.job.isActive.toggle() + } + } + + func removeJob() async { + do { + try await vm.removeJob(id: job.id) + profileVM.shouldRefreshMyPLJobs = true + hud.show("删除成功") + if !naviPath.isEmpty { naviPath.removeLast() } + } catch { + print("删除plJob失败:\(error)") + hud.show("删除失败,请检查网络并重试") + } + } +} diff --git a/IOS_study/Job/PLJobView.swift b/IOS_study/Job/PLJobView.swift new file mode 100644 index 0000000..ed0c135 --- /dev/null +++ b/IOS_study/Job/PLJobView.swift @@ -0,0 +1,145 @@ +// +// PLJobView.swift +// IOS_study +// +// Created by CC-star on 2025/6/13. +// + +import SwiftUI +import Photos + +struct PLJobView: View { + var canEdit = false + var showBoss = true + @Environment(HUD.self) var hud + @Environment(PLJobViewModel.self) var vm + @Environment(ProfileViewModel.self) var profileVM + @Environment(AuthViewModel.self) var authVM + @Environment(ReviewViewModel.self) var reviewVM + @Environment(\.displayScale) var displayScale//获取屏幕分辨率 + @State var showActiveDialog = false//是否上/下线 + @State var showRemoveDialog = false//是否移除 + @State var showRemoveAlert = false//移除警告 + @State var saveImage: UIImage? + @State var openSetting = false + @State var viewDidLoad = false + @State var showReportDialog = false + @State var showPostReview = false + @Binding var naviPath : NavigationPath + var job: PLJob { vm.job } + var isActive:Bool{ vm.job.isActive } + var titleAD: String { isActive ? "" : "[已下线]" } + var isIng: Bool { vm.isRemoving || vm.isToggling } + var shareImage: Image? { if let saveImage { return Image(uiImage: saveImage) } else { return nil } } + var body: some View { + List { + tilteView//头部视图 + if showBoss { companyView }//公司视图 + detailView//工作详情 + contactView//应聘电话 + goRevireBtn + listBottomEView() + }.listStyle().navigationTitle("长期兼职").navigationBarTitleDisplayMode(.inline).navigationBarBackButtonHidden() + //顶部返回键 + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button { + if !naviPath.isEmpty {//检查是否为最后一页 + naviPath.removeLast()//返回上一页 + } + } label: { + Image(systemName: "chevron.left").secondary().padding(.horizontal,6) + } + } + ToolbarItem(placement: .topBarTrailing) { + HStack(spacing: 3) { + if !canEdit { + Button("举报") { showReportDialog = true } + .confirmationDialog("确定举报吗?", isPresented: $showReportDialog, titleVisibility: .visible) { + Button("确定") { + Task { + await report() + } + } + Button("取消", role: .cancel) { } + } + } + + Button { Task { await saveImageToAlbum() } } label: { Text("保存") } + if let shareImage { + ShareLink(item: shareImage, preview: SharePreview("以长图形式分享", image: shareImage)) { Text("分享") } + } + }.tc2().size15() + } + } + .alert("请允许保存图片权限", isPresented: $openSetting) { + Button("再想想") { openSetting = false } + Button("去设置") { + if let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url) } + } + } message: { Text("请重新设置照片权限") } + .sheet(isPresented: $showPostReview, content: { + PostReviewView() + }) + .onAppear { + if !viewDidLoad { + Task { generateSnapshot() } + viewDidLoad = true + } + } + } + var shareView: some View { + VStack(spacing: 0) { + Text("招兼职").size18b() + tilteView//头部视图 + companyView //公司视图 + detailView//工作详情 + contactView//应聘电话 + listBottomEView() + Text("\(kAppName)App").accent().bold().padding() + }.padding(.top, 60).padding(.bottom, 30) + .frame(width: UIScreen.main.bounds.size.width)//按照用户的手机屏幕来得到宽度 + .bg2() + } + @MainActor func generateSnapshot() { + let renderer = ImageRenderer(content: shareView) + renderer.scale = displayScale + saveImage = renderer.uiImage + } + @MainActor func saveImageToAlbum() async { + if let saveImage { + let currenStatus = PHPhotoLibrary.authorizationStatus(for: .addOnly) + if currenStatus == .notDetermined {//未申请过权限 + let status = await PHPhotoLibrary.requestAuthorization(for: .addOnly)//添加只向相册添加相片的权限 + if status == .authorized {//第一次同意 + writeToAlbum(saveImage) + } else if currenStatus == .authorized {//之前同意过 + writeToAlbum(saveImage) + } else { openSetting = true } + } else { hud.show("相片保存失败,请退出本页后重试") } + } + } + func writeToAlbum(_ saveImage: UIImage) { UIImageWriteToSavedPhotosAlbum(saveImage, nil, nil, nil); hud.show("相片保存成功,请返回相册查看") } + // MARK: - 举报 + func report() async { + var reports = (UserDefaults.standard.array(forKey: jobReportsKey) as? [String]) ?? [] + if reports.contains(job.id) { + hud.show("已举报,会尽快处理") + } else { + let userID = AuthManager.shared.getUserID() + if !userID.isEmpty { + if reports.count > 50 { reports.removeAll() } + do { + try await JobReportManger.shared.save(report: JobReport(jobID: job.id, jobType: "兼职", jobTitle: job.title, bossID: job.userID, userID: userID)) + reports.append(job.id) + UserDefaults.standard.set(reports, forKey: jobReportsKey) + hud.show("举报成功,请等候处理") + } catch { + print("举报失败:\(error)") + hud.show("举报失败,请检查网络") + } + } else { hud.show("请先登录") } + } + } +} + diff --git a/IOS_study/Job/PLJobViewModel.swift b/IOS_study/Job/PLJobViewModel.swift new file mode 100644 index 0000000..9db0d8c --- /dev/null +++ b/IOS_study/Job/PLJobViewModel.swift @@ -0,0 +1,103 @@ +// +// PLJobViewModel.swift +// IOS_study +// +// Created by CC-star on 2025/6/29. +// + +import Foundation + + +@Observable final class PLJobViewModel { + var jobs: [PLJob] = [] + var currentPage = 0 + var isFcJobs = false + var isJobsLoaded = false + + //分页用 + var fcJobsErrMsg = "" + var isFinshed = false + + var filter = PLFilter()//筛选用,实例化PLFilter + var order = PLOrder.updatedAt//排序用 + var isOF = false //排序筛选完成后滑动到顶部 + + var job = PLJob() + var draftJob = PLJob()//草稿 + var isPosting = false//是否在提交 + var isRemoving = false + var isToggling = false//上/下线 + + @MainActor func getJobs(isResFresh: Bool = false) async { + if isFcJobs { return }//保证当前没有在获取数据状态,防止用户在请求数据时,再次请求数据 + isFcJobs = true//开始获取数据状态 + defer{ isFcJobs = false }//最后关闭获取数据状态 + isJobsLoaded = false + fcJobsErrMsg = "" + if isResFresh { + currentPage = 0 + isFinshed = false + } + do { + let skip = kJobLimit * currentPage + let newJobs = try await PLJobManager.shared.findJobsByREST(limit: kJobLimit, skip: skip, filter: filter, order: order) + if isResFresh { + jobs = newJobs + } else { + jobs.append(contentsOf: newJobs)//讲一个数组添加到另外一个数组 + } + currentPage += 1//分页 + isJobsLoaded = true//防止onappear多次加载 + + if newJobs.count < kJobLimit || newJobs.isEmpty { + isFinshed = true + } + } catch { + if isResFresh { + jobs.removeAll()//置空 + } + fcJobsErrMsg = "\(error)" + print("获取plJobs失败:\(error)") + } + } + @MainActor func postJob(isEditing: Bool = false) async throws { + job = draftJob + // job.updatedAT = Date() + + //处理空格(必填部分) + job.title = job.title.trimmed + job.placeName = job.placeName.trimmed + job.province = job.province.trimmed + job.city = job.city.trimmed + //处理空格(选填部分) + if job.otherTime.isBlank { job.otherTime = "" } else { job.otherTime = job.otherTime.trimmed } + if job.otherNeed.isBlank { job.otherNeed = "" } else { job.otherNeed = job.otherNeed.trimmed } + if job.workContent.isBlank { job.workContent = "" } else { job.workContent = job.workContent.trimmed } + if job.otherBenefit.isBlank { job.otherBenefit = "" } else { job.otherBenefit = job.otherBenefit.trimmed } + job.userID = AuthManager.shared.getUserID() + + //存到数据库 + try await PLJobManager.shared.save(job: job, isEditing: isEditing) + // do { + // try await PLJobManager.shared.saveByREST(job: job)//使用链接进行存储 + // } catch { + // print(error.localizedDescription)//返回中文的报错 + // } + } + + @MainActor func toggleJobStatus(id: String) async throws { + isToggling = true + defer { + isToggling = false + } + job.isActive.toggle() + try await PLJobManager.shared.toggleJobStatus(id: id, isAictive: job.isActive) + } + @MainActor func removeJob(id: String) async throws { + isRemoving = true + defer { isRemoving = false } + try await PLJobManager.shared.removeJob(id: id) + } + +} + diff --git a/IOS_study/Job/PLJobsView+JobUI.swift b/IOS_study/Job/PLJobsView+JobUI.swift new file mode 100644 index 0000000..d0b62c3 --- /dev/null +++ b/IOS_study/Job/PLJobsView+JobUI.swift @@ -0,0 +1,235 @@ +// +// PLJobsView+JobUI.swift +// IOS_study +// +// Created by CC-star on 2025/7/24. +// + +import SwiftUI + +extension PLJobView { + // MARK: - 头部视图 + var tilteView: some View { + VStack(alignment: .leading,spacing: kStackSpacing) { + //第一层 + HStack { + VStack(alignment: .leading) { + HStack { + Text(job.business) + .labelBG2() + if job.updatedAt.daysFromNow <= 7 { Text("新到").labelWA() } + if job.wantWorkNum > 0 { + Text("\(job.wantWorkNum)").size13s() + } + } + Text(job.title + titleAD).tc().size18b() + } + Spacer() + Text("\(job.hourlyWage)").accent().headline() + }.padding(.bottom,-2) + //第二层 + HStack{ + Text(job.placeName) + if job.tax == "不报税" { + Text(job.tax).accentBorder() + } else if job.tax == "报税" { + Text(job.tax).redBorder() + } else { + Text(job.tax).grayBorder() + } + + Text("\(job.updatedAt.toStr)发布").secondary() + } + //第三层 + HStack(spacing: kStackSpacing){ + HStack { + HStack(spacing: kIconSpacing) { + //icon + Image("station").iconStyle() + Text("\(job.province)-\(job.city)").semib() + //导航按钮:一键智能导航 + Button { + let destination = LocationHelper.shared.buildSmartAddress(job: job) + LocationHelper.shared.navigateToDestination(destination: destination) { errorMessage in + if let message = errorMessage { + hud.show(message) + } + } + } label: { + Text("导航") + }.btnStyle() + } + HStack(spacing: kIconSpacing) { + Image("want").iconStyle() + Text("招\(job.wantNum)人") + } + + if canEdit && job.userID == AuthManager.shared.getUserID(){ + HStack { + Button("编辑") { + vm.draftJob = job + naviPath.append(Navi5.PLJobToEdit) + }.buttonStyle(.borderless).accent().disabled(isIng) + + Button { showActiveDialog = true } label: { + if vm.isToggling { ProgressView().controlSize(.small) } else { Text(isActive ? "下线" : "上线") } + }.buttonStyle(.borderless).accent().disabled(isIng) + .confirmationDialog(isActive ? "确定下线吗" : "确定上线吗", isPresented: $showActiveDialog, titleVisibility: .visible) { + Button("确定") { + Task { await toggleJobStatus() } + } + Button("再想想", role: .cancel) { } + } + Button { showRemoveDialog = true } label: { + if vm.isRemoving { ProgressView().controlSize(.small) } else { Text("删除") } + }.buttonStyle(.borderless).accent().disabled(isIng) + .confirmationDialog("确定删除吗", isPresented: $showRemoveDialog, titleVisibility: .visible) { + Button("确定", role: .destructive) { + showRemoveAlert = true + } + Button("再想想", role: .cancel) { } + }.alert("删除之后不可恢复", isPresented: $showRemoveAlert) { + Button("确定", role: .destructive) { Task { await removeJob() } } + Button("取消", role: .cancel) { } + } + } + } + } + } + }.textStyle2().cellStyle() + } + // MARK: - 公司视图 + var companyView: some View { + Button { + //todo + } label: { + HStack(spacing: kIconSpacing){ + Image("company").iconStyle() + Text(job.companyName).size16semib().oneLineStyleMiddle() + Spacer() + Group { + Text("更多工作") + Image(systemName: "chevron.right") + }.secondary() + } + }.textStyle().cellStyle().buttonStyle(.borderless) + } + // MARK: - 工作详情 + var detailView: some View { + VStack(alignment: .leading,spacing:kStackSpacingS) { + //公司时间 + VStack(alignment: .leading,spacing: kStackSpacingS) { + HStack(spacing: kIconSpacing) { + Image("time").iconStyle() + Text("工作时间").subTStyle() + } + + HStack(spacing: 2) { + Text("\(job.startHour)") + Text(":").conlonStyle() + Text(job.startMin == 0 ? "00" : "\(job.startMin)") + Text("-").conlonStyle() + Text("\(job.endHour)") + Text(":").conlonStyle() + Text(job.endMin == 0 ? "00" : "\(job.endMin)") + } + + if job.has2 { + HStack(spacing: 2) { + Text("\(job.startHour2)") + Text(":").conlonStyle() + Text(job.startMin2 == 0 ? "00" : "\(job.startMin2)") + Text("-").conlonStyle() + Text("\(job.endHour2)") + Text(":").conlonStyle() + Text(job.endMin2 == 0 ? "00" : "\(job.endMin2)") + } + + } + + // Text("\(job.startHour)-\(job.endHour)").labelBG() + // if job.has2 { + // Text("\(job.startHour2)-\(job.endHour2)").labelBG() + // } + } + + // Divider() + exDivider() + if !job.workContent.isEmpty { + //工作内容 + VStack(alignment: .leading,spacing: kStackSpacingS) { + HStack(spacing: kIconSpacing) { + Image("content").iconStyle() + Text("工作内容").subTStyle() + } + Text(job.workContent).allowMulti() + } + exDivider() + } + //工作要求 + VStack(alignment: .leading,spacing: kStackSpacingS) { + HStack(spacing: kIconSpacing) { + Image("need").iconStyle() + Text("工作要求").subTStyle() + } + HStack { + Text(job.needExp).labelWAL() + Text(job.needEdu).labelWAL() + } + HStack { + Text(job.needLan).labelWAL() + Text(job.needFrame).labelWAL() + } + if !job.otherNeed.isEmpty { + Text(job.otherNeed).allowMulti() + } + } + + //公式福利 + exDivider() + VStack(alignment: .leading,spacing: kStackSpacingS) { + HStack(spacing: kIconSpacing) { + Image("benefit").iconStyle() + Text("公式福利").subTStyle() + } + HStack { + if job.noOverTime { + Text("不加班").labelWAL() + } + if job.moveFee { + Text("交通费").labelWAL() + } + } + //更多福利 + if !job.otherBenefit.isEmpty { + Text(job.otherBenefit).allowMulti() + } + } + }.textStyle2().cellStyle() + } + // MARK: - 应聘电话 + var contactView: some View { + VStack(alignment: .leading,spacing:kStackSpacingS){ + Text("应聘方式(\(job.contactType))").subTStyleL() + HStack { + Text("\(job.contact)").size20b().tc().allowMulti() + Spacer() + Button("复制"){ + UIPasteboard.general.string = job.contact + hud.show("复制成功") + //todo弹窗提示 + }.btnStyle().padding(6) + + + // Button { + // UIPasteboard.general.string = "0423-1230-1241" + // //todo弹窗提示 + // } + // } label: { + // Text("复制") + // }.btnStyle().padding(6) + } + Text("若问起,请说是在测试软件上看到的").size14().purple() + }.cellStyle() + } +} diff --git a/IOS_study/Jobs/FilterView.swift b/IOS_study/Jobs/FilterView.swift new file mode 100644 index 0000000..a7905f1 --- /dev/null +++ b/IOS_study/Jobs/FilterView.swift @@ -0,0 +1,31 @@ +// +// FilterView.swift +// IOS_study +// +// Created by CC-star on 2025/7/8. +// + +import SwiftUI + +struct FilterView: View { + var body: some View { + VStack(spacing: 14) { + VStack(alignment: .leading) { + Text("行业").labelStyleOF() + LazyVGrid(columns: [GridItem(.adaptive(minimum: 70))]) { + ForEach(kBusinessArr,id: \.self) { business in + Button { + + } label: { + Text(business).frame(maxWidth: .infinity) + }.buttonStyle(.borderless) + } + } + } + }.padding() + } +} + +#Preview { + FilterView() +} diff --git a/IOS_study/Jobs/PLJobCellView.swift b/IOS_study/Jobs/PLJobCellView.swift new file mode 100644 index 0000000..393aacf --- /dev/null +++ b/IOS_study/Jobs/PLJobCellView.swift @@ -0,0 +1,68 @@ +// +// PLJobCellView.swift +// IOS_study +// +// Created by CC-star on 2025/6/12. +// + +import SwiftUI + +struct PLJobCellView: View { + let job: PLJob + var body: some View { + VStack(spacing: 8) { + //第一层 + HStack { + VStack(alignment: .leading) { + HStack { + Text(job.business) + .labelBG2() + if job.updatedAt.daysFromNow <= 7 { Text("新到").labelWA() } + if job.wantWorkNum > 0 { + Text("\(job.wantWorkNum)").size13s() + } + } + HStack { + Text(job.title).tc().headline().oneLineStyleMiddle() + + if job.tax == "不报税" { + Text(job.tax).accentBorder() + } else if job.tax == "报税" { + Text(job.tax).redBorder() + } else { + Text(job.tax).grayBorder() + } + } + } + Spacer() + Text("\(job.hourlyWage)").accent().headline() + } + //第二层 + OverflowGrid(horizontalSpacing: 5) { + Text(job.needExp).labelBG() + Text(job.needEdu).labelBG() + Text(job.needLan).labelBG() + Text(job.needFrame).labelBG() + Text("\(job.startHour)-\(job.endHour)").labelBG() + if job.has2 { + Text("\(job.startHour2)-\(job.endHour2)").labelBG() + } + if job.moveFee { Text("交通费").labelPP() } + if job.noOverTime { Text("不加班").labelPP() } + + }.push(to: .leading) + + //第三层 + HStack { + Text(job.placeName).tc().lineLimit(1) + Spacer() + Text("\(job.province)-\(job.city)").accent().semib().oneLineStyleMiddle() + }.size14() + } + .cellPaddingRadius().cellOutPadding() + } +} + +#Preview { + PLJobCellView(job: PLJob.job) +} diff --git a/IOS_study/Jobs/PLJobsView+.swift b/IOS_study/Jobs/PLJobsView+.swift new file mode 100644 index 0000000..591df40 --- /dev/null +++ b/IOS_study/Jobs/PLJobsView+.swift @@ -0,0 +1,118 @@ +// +// PLJobsView+.swift,主要放置筛选、排序 +// IOS_study +// +// Created by CC-star on 2025/7/10. +// + +import SwiftUI + +extension PLJobsView { + //筛选 + var filterBtn: some View { + Button { + showOrderView = false + showFilterView.toggle()//反转 + } label: { + HStack(spacing: 3) { + Text("筛选") + if vm.filter.totalCount > 0 { + Text("\(vm.filter.totalCount)").badgeStyle() + } + arrowIcon(showFilterView) + }.tc2().size15() + }.disabled(vm.isFcJobs) + } + + var plFilterView: some View { + VStack(spacing: 14) { + VStack(alignment: .leading) { + Text("行业").labelStyleOF() + LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))]) { + ForEach(kBusinessArr,id: \.self) { business in + Button { + if vm.filter.business == business { + vm.filter.business = nil + } else { + vm.filter.business = business + } + } label: { + Text(business).frame(maxWidth: .infinity) + }.selectBtnStyle(vm.filter.business == business) + } + } + } + VStack(alignment: .leading) { + Text("城市").labelStyleOF() + LazyVGrid(columns: [GridItem(.adaptive(minimum: 70))]) { + ForEach(kCityArr,id: \.self) { city in + Button { + if vm.filter.city == city { + vm.filter.city = nil + } else { + vm.filter.city = city + } + } label: { + Text(city).frame(maxWidth: .infinity) + }.selectBtnStyle(vm.filter.city == city) + } + } + } + HStack { + Button { + vm.filter = PLFilter() + } label: { + Text("清空").appleStyleS() + }.buttonStyle(.bordered) + Button { + showFilterView = false + getJobsByOF() + } label: { + Text("筛选").appleStyleS() + }.tint(.aTC).buttonStyle(.borderedProminent) + }.padding(.top, 9) + }.size14().padding().bg() + } + //排序 + var orderBtn: some View { + Button { + showFilterView = false + showOrderView.toggle()//反转 + } label: { + HStack(spacing: 3) { + Text(vm.order.rawValue) + arrowIcon(showOrderView) + }.tc2().size15() + }.disabled(vm.isFcJobs) + } + //排序视图 + var plOrderView: some View { + VStack(spacing: 14) { + ForEach(PLOrder.allCases.indices, id: \.self) { index in + let order = PLOrder.allCases[index] + HStack { + Text(order.rawValue).foregroundStyle(vm.order == order ? .accent : .aTC) + Spacer() + if vm.order == order { Image(systemName: "checkmark.circle.fill").accent() } + }.contentShape(Rectangle())//指定点击区域为整个矩形 + .onTapGesture { + vm.order = order + showOrderView = false + getJobsByOF() + } + if order != PLOrder.allCases.last { exDivider() } + } + }.size15().padding().bg() + } + + func getJobsByOF() {//筛选功能 + vm.isOF.toggle() + if vm.filter == lastPLFilter && vm.order == lastPLOrder { return }//如果与上次筛选\排序的选择一样,则不请求 + lastPLFilter = vm.filter + lastPLOrder = vm.order + Task { + await vm.getJobs(isResFresh: true)//刷新 + } + } + +} diff --git a/IOS_study/Jobs/PLJobsView.swift b/IOS_study/Jobs/PLJobsView.swift new file mode 100644 index 0000000..1d770da --- /dev/null +++ b/IOS_study/Jobs/PLJobsView.swift @@ -0,0 +1,158 @@ +// +// PLJobsView.swift +// IOS_study +// +// Created by CC-star on 2025/6/12. +// + +import SwiftUI +import LeanCloud + +struct PLJobsView: View { + @State var lastPLFilter = PLFilter() + @State var showFilterView = false + @State var showOrderView = false + @State var lastPLOrder: PLOrder = .updatedAt + @Environment(HUD.self) var hud + @Environment(PLJobViewModel.self) var vm + @Environment(TabBarViewModel.self) var TabBarVM + @Environment(NetworkMonitor.self) var nt + @State var naviPath = NavigationPath() + var body: some View { + NavigationStack(path: $naviPath) { + ZStack { + if !vm.jobs.isEmpty { + ScrollViewReader { scrollView in + List{ + Color.clear.frame(height: 1).listRowStyle().id("top")//透明空白视图,无明显影响 + ForEach(vm.jobs) {job in + Button { + vm.job = job + naviPath.append(Navi1.PLJobsToPLJob) + } label: { + PLJobCellView(job: job) + }.buttonStyle(.borderless) + }.listRowStyle() + + // Button("加载更多", action: { + // loadMore() + // }).buttonStyle(.borderless) + Group { + if vm.fcJobsErrMsg.isEmpty { + if !vm.isFinshed { + if vm.isFcJobs { Text("正在加载中...") } else { Text("正在加载中...").onAppear { loadMore() } } + } else { + if nt.isConnected { + Text("已全部加载") + } else { + Text("当前无网络,无法加载更多数据,请联网后下拉刷新") + } + } + } else { + HStack { + Text("自动加载数据失败,请手动加载") + Button("点击加载", action: loadMore).buttonStyle(.borderless).accent() + } + } + }.listRowStyleLoading2() + }.listStyle().refreshable { await refreashable() } + .onChange(of: vm.isOF) {//尾随闭包 + withAnimation { scrollView.scrollTo("top", anchor: .top) } + } + } + } else { + //首次加载,还没有加载出数据 + if vm.fcJobsErrMsg.isEmpty {//没有错误 + if !vm.isFcJobs { + if nt.isConnected && vm.filter.totalCount > 0 { + //筛选后没有数据todo + Text("没有找到符合筛选条件的在招职位,请更改筛选条件").explainStyle() + } else {//取完数据后发现没有数据:1.没有联网 2.筛选后没有数据 + VStack(spacing: 9) {//没有联网【根据后端SDK的不同,有的直接走这里,有的直接抛出错误】 + Button { + refreash() + } label: { + Text("点击刷新").appleStyle() + } + Text("网络问题,若刷新失败,请重启App").explainStyle() + } + } + } else { + Color.bg2//展示空白视图 + } + } else {//有错误 + VStack(alignment: .leading, spacing: 9) { + Button { + refreash() + } label: { + Text("点击刷新").appleStyle() + } + Text("加载数据失败,请点击刷新") + HStack { + Text("若刷新失败,请截图并联系客服微信:\(KdevContact)") + Button { + UIPasteboard.general.string = KdevContact + hud.show("复制成功") + } label: { + Text("复制").accent() + } + } + exDivider() + // HStack { + Text("错误信息").push(to: .center) + Text(vm.fcJobsErrMsg).red() + // } + }.explainStyle() + } + } + }.naviBarStyle() + .navigationDestination(for: Navi1.self) { navi in + switch navi { + case .PLJobsToPLJob : PLJobView(naviPath: $naviPath) + } + } + .toolbar { + ToolbarItem(placement: .topBarLeading) { orderBtn } + ToolbarItem(placement: .principal) { Text("好工作").tc().bold() } + ToolbarItem(placement: .topBarTrailing) { filterBtn } + } + .overlay { + if showFilterView || showOrderView {//展示筛选\排序视图 + Color.black.opacity(0.3)//半透明阴影视图 + .onTapGesture {//点击手势 + showFilterView = false + showOrderView = false + } + } + } + .overlay(alignment: .top) { + if showFilterView { plFilterView } else if showOrderView { plOrderView } + } + .overlay { if vm.isFcJobs { BprogressView() } } + }.animation(.default, value: showFilterView)//对showFilterView使用默认【default】的动画效果 + .onAppear { + getJobsOnce() + refreshIFNeed() + } + } + + func getJobsOnce() { + if vm.isJobsLoaded { return } + Task { await vm.getJobs() } + } + func refreshIFNeed() { + if vm.isJobsLoaded && TabBarVM.refreshJobs && TabBarVM.jobType == "兼职" { + vm.filter = PLFilter() + vm.order = .updatedAt + refreash() + TabBarVM.refreshJobs = false + } + } + func refreashable() async { await vm.getJobs(isResFresh: true) } + func refreash() { Task { await refreashable() } } + func loadMore() { Task { await vm.getJobs() } } +} + +#Preview { + PLJobsView() +} diff --git a/IOS_study/Library/HUDView.swift b/IOS_study/Library/HUDView.swift new file mode 100644 index 0000000..3f4c4c4 --- /dev/null +++ b/IOS_study/Library/HUDView.swift @@ -0,0 +1,45 @@ +// +// HUDView.swift +// 界面悬浮提示框 +// +// Created by CC-star on 2025/7/15. +// + +import SwiftUI +import Combine +// 悬浮提示框 +extension View { + func hud (isPresented: Binding, @ViewBuilder content: () -> Content) -> some View { + ZStack { + self + if isPresented.wrappedValue { + HUDView(content: content).zIndex(1) + } + }.animation(.default, value: isPresented.wrappedValue) + } +} +struct HUDView: View { + @ViewBuilder let content: Content + var body: some View { + content.font(.subheadline).foregroundStyle(Color.aTCR) + .padding(.horizontal, 4).padding(12) + .background(RoundedRectangle(cornerRadius: 8).fill(Color.aBGR)) + } +} +@Observable final class HUD { + var isPresented = false + private(set) var title: String = "" + private var dismissTimer: AnyCancellable? // 用于管理计时器-追加代码-因原版本多次按下后消失不了 + func show(_ title: String) { + self.title = title + isPresented = true + dismissTimer?.cancel()// 取消之前的计时器,确保每次调用时重置-追加代码 + // 3秒后自动隐藏-追加代码 + dismissTimer = Timer.publish(every: 3, on: .main, in: .common) + .autoconnect() + .sink { [weak self] _ in + self?.isPresented = false + self?.dismissTimer?.cancel() // 确保计时器在执行后被取消 + } + } +} diff --git a/IOS_study/Library/NetworkMonitor.swift b/IOS_study/Library/NetworkMonitor.swift new file mode 100644 index 0000000..0b231d4 --- /dev/null +++ b/IOS_study/Library/NetworkMonitor.swift @@ -0,0 +1,22 @@ +// +// NetworkMonitor.swift +// 网络监控 +// +// Created by CC-star on 2025/7/8. +// + +import Foundation +import Network + +@Observable final class NetworkMonitor { + private let networkMonitor = NWPathMonitor() + private let workerQueue = DispatchQueue(label: "Monitor") + var isConnected = false + + init() { + networkMonitor.pathUpdateHandler = { path in + self.isConnected = path.status == .satisfied + } + networkMonitor.start(queue: workerQueue) + } +} diff --git a/IOS_study/Library/OverflowGrid.swift b/IOS_study/Library/OverflowGrid.swift new file mode 100644 index 0000000..43dcbb1 --- /dev/null +++ b/IOS_study/Library/OverflowGrid.swift @@ -0,0 +1,50 @@ +import SwiftUI + +//自动换行 +//https://stackoverflow.com/questions/58842453/hstack-with-wrap RelativeJoe的回答 +public struct OverflowGrid: Layout { + private var horizontalSpacing: CGFloat + private var vericalSpacing: CGFloat + public init(horizontalSpacing: CGFloat, vericalSpacing: CGFloat? = nil) { + self.horizontalSpacing = horizontalSpacing + self.vericalSpacing = vericalSpacing ?? horizontalSpacing + } + public func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + let height = subviews.max(by: {$0.dimensions(in: proposal).height > $1.dimensions(in: proposal).height})?.dimensions(in: proposal).height ?? 0 + var rows = [CGFloat]() + subviews.indices.forEach { index in + let rowIndex = rows.count - 1 + let subViewWidth = subviews[index].dimensions(in: proposal).width + guard !rows.isEmpty else { + rows.append(subViewWidth) + return + } + let newWidth = rows[rowIndex] + subViewWidth + horizontalSpacing + if newWidth < proposal.width ?? 0 { + rows[rowIndex] += (rows[rowIndex] > 0 ? horizontalSpacing: 0) + subViewWidth + }else { + rows.append(subViewWidth) + } + } + let count = CGFloat(rows.count) + return CGSize(width: rows.max() ?? 0, height: count * height + (count - 1) * vericalSpacing) + + } + public func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { + let height = subviews.max(by: {$0.dimensions(in: proposal).height > $1.dimensions(in: proposal).height})?.dimensions(in: proposal).height ?? 0 + guard !subviews.isEmpty else {return} + var x = bounds.minX + var y = height/2 + bounds.minY + subviews.indices.forEach { index in + let subView = subviews[index] + x += subView.dimensions(in: proposal).width/2 + subviews[index].place(at: CGPoint(x: x, y: y), anchor: .center, proposal: ProposedViewSize(width: subView.dimensions(in: proposal).width, height: subView.dimensions(in: proposal).height)) + x += horizontalSpacing + subView.dimensions(in: proposal).width/2 + if x > bounds.width { + x = bounds.minX + y += height + vericalSpacing + } + } + } +} + diff --git a/IOS_study/Library/PinchZoomView.swift b/IOS_study/Library/PinchZoomView.swift new file mode 100644 index 0000000..2a8f729 --- /dev/null +++ b/IOS_study/Library/PinchZoomView.swift @@ -0,0 +1,139 @@ +// +// PinchZoomView.swift +// 双指捏合放大扩展 +// +// Created by CC-star on 2025/7/22. +// + +import SwiftUI + +//图片查看器-用法: +//Image("Zoom").pinchToZoom() +class PinchZoomView: UIView { + weak var delegate: PinchZoomViewDelgate? + private(set) var scale: CGFloat = 0 { + didSet { delegate?.pinchZoomView(self, didChangeScale: scale) } + } + + private(set) var anchor: UnitPoint = .center { + didSet { delegate?.pinchZoomView(self, didChangeAnchor: anchor) } + } + + private(set) var offset: CGSize = .zero { + didSet { delegate?.pinchZoomView(self, didChangeOffset: offset) } + } + + private(set) var isPinching: Bool = false { + didSet { delegate?.pinchZoomView(self, didChangePinching: isPinching) } + } + + private var startLocation: CGPoint = .zero + private var location: CGPoint = .zero + private var numberOfTouches: Int = 0 + + init() { + super.init(frame: .zero) + let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinch(gesture:))) + pinchGesture.cancelsTouchesInView = false + addGestureRecognizer(pinchGesture) + } + + required init?(coder: NSCoder) { fatalError() } + + @objc private func pinch(gesture: UIPinchGestureRecognizer) { + switch gesture.state { + case .began: + isPinching = true + startLocation = gesture.location(in: self) + anchor = UnitPoint(x: startLocation.x / bounds.width, y: startLocation.y / bounds.height) + numberOfTouches = gesture.numberOfTouches + case .changed: + if gesture.numberOfTouches != numberOfTouches { + // If the number of fingers being used changes, the start location needs to be adjusted to avoid jumping. + let newLocation = gesture.location(in: self) + let jumpDifference = CGSize(width: newLocation.x - location.x, height: newLocation.y - location.y) + startLocation = CGPoint(x: startLocation.x + jumpDifference.width, y: startLocation.y + jumpDifference.height) + + numberOfTouches = gesture.numberOfTouches + } + scale = gesture.scale + location = gesture.location(in: self) + offset = CGSize(width: location.x - startLocation.x, height: location.y - startLocation.y) + case .ended, .cancelled, .failed: + isPinching = false + //加动画--在源代码上改动的地方 + //withAnimation(.interactiveSpring(response: 0.2, dampingFraction: 0.75, blendDuration: 0.1)) + withAnimation(.interactiveSpring(response: 0.2, dampingFraction: 0.65, blendDuration: 0.1)) { + scale = 1.0 + anchor = .center + offset = .zero + } + default: break + } + } + +} + +protocol PinchZoomViewDelgate: AnyObject { + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangePinching isPinching: Bool) + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeScale scale: CGFloat) + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeAnchor anchor: UnitPoint) + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeOffset offset: CGSize) +} + +struct PinchZoom: UIViewRepresentable { + @Binding var scale: CGFloat + @Binding var anchor: UnitPoint + @Binding var offset: CGSize + @Binding var isPinching: Bool + + func makeCoordinator() -> Coordinator { Coordinator(self) } + func makeUIView(context: Context) -> PinchZoomView { + let pinchZoomView = PinchZoomView() + pinchZoomView.delegate = context.coordinator + return pinchZoomView + } + func updateUIView(_ pageControl: PinchZoomView, context: Context) { } + class Coordinator: NSObject, PinchZoomViewDelgate { + var pinchZoom: PinchZoom + init(_ pinchZoom: PinchZoom) { self.pinchZoom = pinchZoom } + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangePinching isPinching: Bool) { + pinchZoom.isPinching = isPinching + } + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeScale scale: CGFloat) { + pinchZoom.scale = scale + } + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeAnchor anchor: UnitPoint) { + pinchZoom.anchor = anchor + } + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeOffset offset: CGSize) { + pinchZoom.offset = offset + } + } +} +struct PinchToZoom: ViewModifier { + @State var scale: CGFloat = 1.0 + @State var anchor: UnitPoint = .center + @State var offset: CGSize = .zero + @State var isPinching: Bool = false + func body(content: Content) -> some View { + content.scaleEffect(scale, anchor: anchor).offset(offset) + //原代码中有这个,但效果不好,需要在上面pinch中加withanimation才行,故注释 + // .animation(isPinching ? .none : .spring(), value: scale) + // .animation(.spring(duration: 1), value: scale) + .overlay(PinchZoom(scale: $scale, anchor: $anchor, offset: $offset, isPinching: $isPinching)) + // .onChange(of: isPinching) { + // if !isPinching { + // // 在松开手势时触发动画恢复 + // withAnimation(.spring()) { + // scale = 1.0 + // offset = .zero + // anchor = .center + // } + // } + // } + } +} +extension View { + func pinchToZoom() -> some View { self.modifier(PinchToZoom()) } +} diff --git a/IOS_study/Manager/AuthManager.swift b/IOS_study/Manager/AuthManager.swift new file mode 100644 index 0000000..63de062 --- /dev/null +++ b/IOS_study/Manager/AuthManager.swift @@ -0,0 +1,29 @@ +// +// AuthManager.swift +// IOS_study +// +// Created by CC-star on 2025/7/10. +// + +import Foundation +import LeanCloud + +final class AuthManager { + static let shared = AuthManager() + private init() {} + + func getVerCode(phoneNum: String, tName: String, sName: String) async throws { + try await LCSMSClient.requestShortMessage(mobilePhoneNumber: phoneNum, templateName: tName, signatureName: sName) + } + + func login(phoneNum: String, verCode: String) async throws -> LCUser { + try await LCUser.signUpOrLogIn(mobilePhoneNumber: phoneNum, verificationCode: verCode) + } + func getUserID() -> String { + LCApplication.default.currentUser?.objectId?.value ?? ""//获取dbuser的id + } + func getUser() -> LCUser? { LCApplication.default.currentUser } + func logOut() { + LCUser.logOut() + } +} diff --git a/IOS_study/Manager/PLJobManager.swift b/IOS_study/Manager/PLJobManager.swift new file mode 100644 index 0000000..a384446 --- /dev/null +++ b/IOS_study/Manager/PLJobManager.swift @@ -0,0 +1,194 @@ +// +// PLJobManager.swift +// IOS_study +// +// Created by CC-star on 2025/7/2. +// + +import Foundation +import LeanCloud + +final class PLJobManager { + //单例模式 + static var shared = PLJobManager() + private init() {} + private let className = "PLJob" + // //仅供学习 + // func findJobs() async throws -> [PLJob] { + // let query = LCQuery(className: className) + // query.whereKey(PLJob.CodingKeys.isActive.rawValue,.equalTo(true))//equalTo -> = + // query.whereKey("updatedAt", .descending)//降序 + // + // return try await query.find(as: PLJob.self) + // } + // MARK: - 取出所有工作 + func findJobsByREST(limit: Int, skip: Int, filter: PLFilter, order: PLOrder) async throws -> [PLJob] { + // let urlStr = """ + // \(kServerURLStr)/1.1/classes/\(className)?where={\(PLJob.CodingKeys.isActive.rawValue): true }&order=-updatedAt + // """ + var components = URLComponents(string: "\(kServerURLStr)/1.1/classes/\(className)")! + + //筛选 + var whereDic: [String: Any] = [PLJob.CodingKeys.isActive.rawValue: true] + if let business = filter.business {//按行业筛选 + whereDic[PLJob.CodingKeys.business.rawValue] = business + } + if let city = filter.city {//按城市筛选 + whereDic[PLJob.CodingKeys.city.rawValue] = city + } + //转换为字符串 + let whereData = try JSONSerialization.data(withJSONObject: whereDic) + guard let whereStr = String(data: whereData, encoding: .utf8) else { throw URLError(.badURL) } + + //排序 + var orderStr = "" + switch order { + case .updatedAt: + orderStr = "-updatedAt" + case .hourlywage: + orderStr = "-hourlywage" + } + + components.queryItems = [ + URLQueryItem(name: "where", value: whereStr), + URLQueryItem(name: "order", value: orderStr), + URLQueryItem(name: "limit", value: "\(limit)"), + URLQueryItem(name: "skip", value: "\(skip)") + ] + guard let urlstr = components.url?.absoluteString else { throw URLError(.badURL) } + + return try await RESTManager.shared.find(form: urlstr) + } + // MARK: - 取出我发布的工作 + func findJobsByREST(by userID: String, limit: Int, skip: Int) async throws -> [PLJob] { + var components = URLComponents(string: "\(kServerURLStr)/1.1/classes/\(className)")! + + //基础条件 + let whereDic: [String: Any] = [PLJob.CodingKeys.userID.rawValue: userID] + + //转换为字符串 + let whereData = try JSONSerialization.data(withJSONObject: whereDic) + guard let whereStr = String(data: whereData, encoding: .utf8) else { throw URLError(.badURL) } + + //添加查询参数 + components.queryItems = [ + URLQueryItem(name: "where", value: whereStr), + URLQueryItem(name: "order", value: "-updatedAt"), + URLQueryItem(name: "limit", value: "\(limit)"), + URLQueryItem(name: "skip", value: "\(skip)") + ] + guard let urlstr = components.url?.absoluteString else { throw URLError(.badURL) } + + return try await RESTManager.shared.find(form: urlstr) + } + // MARK: - 取出job数量【已上线/全部】 + func findJobsCount(by userID: String, onlyActive: Bool = false) async -> Int { + let query = LCQuery(className: className) + query.whereKey(PLJob.CodingKeys.userID.rawValue, .equalTo(userID)) + if onlyActive { query.whereKey(PLJob.CodingKeys.isActive.rawValue, .equalTo(true)) } + return (try? await query.count()) ?? 0 + } + + func save(job: PLJob, isEditing: Bool = false) async throws { + // 构建对象 + var lcObject = LCObject(className: "PLJob") + + if isEditing { + let query = LCQuery(className: className) + query.whereKey("id", .equalTo(job.id)) + lcObject = try await query.getFirst() + } + + // 为属性赋值 + try lcObject.set(PLJob.CodingKeys.id.rawValue, value: job.id) + try lcObject.set(PLJob.CodingKeys.isActive.rawValue, value: job.isActive) + try lcObject.set(PLJob.CodingKeys.title.rawValue, value: job.title) + try lcObject.set(PLJob.CodingKeys.business.rawValue, value: job.business) + try lcObject.set(PLJob.CodingKeys.tax.rawValue, value: job.tax) + try lcObject.set(PLJob.CodingKeys.hourlyWage.rawValue, value: job.hourlyWage) + try lcObject.set(PLJob.CodingKeys.wantNum.rawValue, value: job.wantNum) + try lcObject.set(PLJob.CodingKeys.contactType.rawValue, value: job.contactType) + try lcObject.set(PLJob.CodingKeys.contact.rawValue, value: job.contact) + + //工作时间 + try lcObject.set(PLJob.CodingKeys.startHour.rawValue, value: job.startHour) + try lcObject.set(PLJob.CodingKeys.startMin.rawValue, value: job.startMin) + try lcObject.set(PLJob.CodingKeys.endHour.rawValue, value: job.endHour) + try lcObject.set(PLJob.CodingKeys.endMin.rawValue, value: job.endMin) + try lcObject.set(PLJob.CodingKeys.has2.rawValue, value: job.has2) + try lcObject.set(PLJob.CodingKeys.startHour2.rawValue, value: job.startHour2) + try lcObject.set(PLJob.CodingKeys.startMin2.rawValue, value: job.startMin2) + try lcObject.set(PLJob.CodingKeys.endHour2.rawValue, value: job.endHour2) + try lcObject.set(PLJob.CodingKeys.endMin2.rawValue, value: job.endMin2) + try lcObject.set(PLJob.CodingKeys.otherTime.rawValue, value: job.otherTime) + + //工作地点 + try lcObject.set(PLJob.CodingKeys.placeName.rawValue, value: job.placeName) + try lcObject.set(PLJob.CodingKeys.province.rawValue, value: job.province) + try lcObject.set(PLJob.CodingKeys.city.rawValue, value: job.city) + try lcObject.set(PLJob.CodingKeys.workContent.rawValue, value: job.workContent) + + //要求 + try lcObject.set(PLJob.CodingKeys.needExp.rawValue, value: job.needExp) + try lcObject.set(PLJob.CodingKeys.needEdu.rawValue, value: job.needEdu) + try lcObject.set(PLJob.CodingKeys.needLan.rawValue, value: job.needLan) + try lcObject.set(PLJob.CodingKeys.needFrame.rawValue, value: job.needFrame) + try lcObject.set(PLJob.CodingKeys.otherNeed.rawValue, value: job.otherNeed) + + //福利 + try lcObject.set(PLJob.CodingKeys.moveFee.rawValue, value: job.moveFee) + try lcObject.set(PLJob.CodingKeys.noOverTime.rawValue, value: job.noOverTime) + try lcObject.set(PLJob.CodingKeys.otherBenefit.rawValue, value: job.otherBenefit) + + //其他 + try lcObject.set(PLJob.CodingKeys.companyName.rawValue, value: job.companyName) + try lcObject.set(PLJob.CodingKeys.userID.rawValue, value: job.userID) + // try lcObject.set("updatedAT", value: job.updatedAT) + + // 将对象保存到云端 + // _ = lcObject.save { result in + // switch result { + // case .success: + // // 成功保存之后,执行其他逻辑 + // break + // case .failure(error: let error): + // // 异常处理 + // print(error) + // } + // } + + try await lcObject.save() + } + + func saveByREST(job: PLJob) async throws { + // + // guard let url = URL(string: "\(kServerURLStr)/1.1/classes/PLJob") else { + // throw NetworkError.invalidURL//抛出问题 -> 无效网址 + // }//有值则赋值给url,没有则进入else + // + try await RESTManager.shared.save(to: "\(kServerURLStr)/1.1/classes/\(className)", object: job) + } + // MARK: - 上/下线工作 + func toggleJobStatus(id: String, isAictive: Bool) async throws { + let query = LCQuery(className: className) + query.whereKey("id", .equalTo(id)) + let lcObject = try await query.getFirst() + try lcObject.set(PLJob.CodingKeys.isActive.rawValue, value: isAictive) + try await lcObject.save() + } + // MARK: - 删除工作 + func removeJob(id: String) async throws { + let query = LCQuery(className: className) + query.whereKey("id", .equalTo(id)) + let lcObject = try await query.getFirst() + try await lcObject.delete() + } + // MARK: -删除某个用户发布的所有job + func removeJobs(by userID: String) async { + let query = LCQuery(className: className) + query.whereKey(PLJob.CodingKeys.userID.rawValue, .equalTo(userID)) + guard let jobObjects = try? await query.find() else { return } + try? await LCObject.delete(jobObjects)//批量删除 + } + +} diff --git a/IOS_study/Manager/RESTManager.swift b/IOS_study/Manager/RESTManager.swift new file mode 100644 index 0000000..c435890 --- /dev/null +++ b/IOS_study/Manager/RESTManager.swift @@ -0,0 +1,106 @@ +// +// RESTManager.swift +// IOS_study +// +// Created by CC-star on 2025/7/3. +// + +import Foundation + +final class RESTManager { + static let shared = RESTManager() + private init() {}//让RESTManager只在这个class在内部被实例化,不在外部实例化,只能在这个作用域里面被调用 + //通过REST API获取一条数据 + func findOne(form urlStr: String) async throws -> T { + guard let url = URL(string: urlStr) else { throw URLError(.badURL) } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(kAppID, forHTTPHeaderField: "X-LC-Id") + request.setValue(kAppKey, forHTTPHeaderField: "X-LC-Key") + + let (data, response) = try await URLSession.shared.data(for: request) + //print(String(data: data, encoding: .utf8) ?? "Invalid data") + + guard let httpRespsonse = response as? HTTPURLResponse, 200..<300 ~= httpRespsonse.statusCode else { throw URLError(.badServerResponse) } + + let decoder = JSONDecoder() + //decoder.keyDecodingStrategy = .convertFromSnakeCase//数据库字段是下划线写法的时候 + //把数据库里面的时间戳字符串转换成Date类型(了解) + decoder.dateDecodingStrategy = .custom { decoder -> Date in + let container = try decoder.singleValueContainer() + let dateString = try container.decode(String.self) + let isoFormatter = ISO8601DateFormatter() + isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] // 支持毫秒 + if let date = isoFormatter.date(from: dateString) { return date } + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date format: \(dateString)") + } + return try decoder.decode( T.self, from: data) + } + + + //通过REST API获取数组数据 + func find(form urlStr: String) async throws ->[T] { + guard let url = URL(string: urlStr) else { throw URLError(.badURL) } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(kAppID, forHTTPHeaderField: "X-LC-Id") + request.setValue(kAppKey, forHTTPHeaderField: "X-LC-Key") + + let (data,_) = try await URLSession.shared.data(for: request) + //print(String(data: data, encoding: .utf8) ?? "Invalid data") + + guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], + let results = json["results"] as? [[String: Any]] else { + throw URLError(.cannotParseResponse) + } + + let decoder = JSONDecoder() + //decoder.keyDecodingStrategy = .convertFromSnakeCase//数据库字段是下划线写法的时候 + //把数据库里面的时间戳字符串转换成Date类型(了解) + decoder.dateDecodingStrategy = .custom { decoder -> Date in + let container = try decoder.singleValueContainer() + let dateString = try container.decode(String.self) + let isoFormatter = ISO8601DateFormatter() + isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] // 支持毫秒 + if let date = isoFormatter.date(from: dateString) { return date } + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date format: \(dateString)") + } + + let tData = try JSONSerialization.data(withJSONObject: results) + return try decoder.decode([T].self, from: tData) + } + + + //通过REST API上传数据 + func save(to urlStr: String, object: T) async throws {//T:代表通用类型,泛型,Encodable编码 + guard let url = URL(string: urlStr) else { throw NetworkError.invalidURL//抛出问题 -> 无效网址 + }//有值则赋值给url,没有则进入else + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(kAppID, forHTTPHeaderField: "X-LC-Id") + request.setValue(kAppKey, forHTTPHeaderField: "X-LC-Key") + + let encoder = JSONEncoder() +// encoder.keyEncodingStrategy = .convertToSnakeCase//数据字段是下划线的写法的时候 + request.httpBody = try encoder.encode(object)//注意:使用链接的方式请求LeanCloud REST需要去掉Date字段(或其他处理) + + let (data, response) = try await URLSession.shared.data(for: request)//向服务器请求数据,并返回一个元组类型【一个数据,一个响应】 + + guard let httpRespsonse = response as? HTTPURLResponse else { throw NetworkError.notHTTPResponse } + + if !(200..<300).contains(httpRespsonse.statusCode) {//如果不是 2开头的响应码 -> [200,300) + let errRespsonse = try JSONSerialization.jsonObject(with: data) as? [String: Any]//Any:字典类型 + let errMsg = errRespsonse?["error"] as? String ?? "响应数据类型转换失败" + //如果errRespsonse没有值,则返回error + //如果上面的errRespsonse?["error"] as? String都不满足则返回:响应数据类型转换失败,【??】空壳运算符 + + throw NetworkError.requestFailed("错误码:\(httpRespsonse.statusCode),错误信息:\(errMsg)") + } + } +} diff --git a/IOS_study/Manager/Reportmanager.swift b/IOS_study/Manager/Reportmanager.swift new file mode 100644 index 0000000..6082f65 --- /dev/null +++ b/IOS_study/Manager/Reportmanager.swift @@ -0,0 +1,25 @@ +// +// Reportmanager.swift +// IOS_study +// +// Created by CC-star on 2025/7/24. +// + +import Foundation +import LeanCloud + +class JobReportManger { + static let shared = JobReportManger() + private init() {} + private let className = "JobReport" + + func save(report: JobReport) async throws { + let reportObject = LCObject(className: className) + try reportObject.set(JobReport.CodingKeys.jobID.rawValue, value: report.jobID) + try reportObject.set(JobReport.CodingKeys.jobType.rawValue, value: report.jobType) + try reportObject.set(JobReport.CodingKeys.jobTitle.rawValue, value: report.jobTitle) + try reportObject.set(JobReport.CodingKeys.bossID.rawValue, value: report.bossID) + try reportObject.set(JobReport.CodingKeys.userID.rawValue, value: report.userID) + try await reportObject.save() + } +} diff --git a/IOS_study/Manager/StorageManager.swift b/IOS_study/Manager/StorageManager.swift new file mode 100644 index 0000000..2a0771e --- /dev/null +++ b/IOS_study/Manager/StorageManager.swift @@ -0,0 +1,29 @@ +// +// StorageManager.swift +// IOS_study +// +// Created by CC-star on 2025/7/15. +// + +import UIKit +import LeanCloud + +final class StorageManager { + static let shared = StorageManager() + private init() { } + + func saveImage(_ image: UIImage) async throws -> String { + if let data = image.heicData() { + + if Double(data.count) / (1024 * 1024) >= 4 {//大于4MB + throw UserError.imageTooBig + } + + let file = LCFile(payload: .data(data: data)) + file.mimeType = "image/heic" + return try await file.save() + } else { + throw UserError.imageToheicDataError + } + } +} diff --git a/IOS_study/Manager/UserManager.swift b/IOS_study/Manager/UserManager.swift new file mode 100644 index 0000000..7bd0185 --- /dev/null +++ b/IOS_study/Manager/UserManager.swift @@ -0,0 +1,66 @@ +// +// UserManager.swift +// IOS_study +// +// Created by CC-star on 2025/7/14. +// + +import Foundation +import LeanCloud + +final class UserManager { + static var shared = UserManager() + private init() {} + private let className = "DBUser" + + func findUserByREST() async throws -> DBUser { + let query = LCQuery(className: className) + query.whereKey("id", .equalTo(AuthManager.shared.getUserID())) + guard let objectId = (try await query.getFirst()).objectId?.value else { throw UserError.objectIdNotFound } + let urlStr = "\(kServerURLStr)/1.1/classes/\(className)/\(objectId)" + return try await RESTManager.shared.findOne(form: urlStr) + } + + func create(user: LCUser) async throws { + let dbUser = LCObject(className: className) + try dbUser.set(DBUser.CodingKeys.id.rawValue, value: user.objectId) + var nickName = "" + if let objectId = user.objectId?.value { + nickName = objectId.suffix(4).uppercased()//取后四位,并字母大写 + } else { + nickName = String.randomString(4) + } + try dbUser.set(DBUser.CodingKeys.nickName.rawValue, value: "数据猫用户\(nickName)") + try await dbUser.save() + } + + func update(user: DBUser) async throws { + let query = LCQuery(className: className) + query.whereKey("id", .equalTo(AuthManager.shared.getUserID())) + let userObject = try await query.getFirst() + + try userObject.set(DBUser.CodingKeys.cName.rawValue, value: user.cName) + try userObject.set(DBUser.CodingKeys.cLogoURLStr.rawValue, value: user.cLogoURLStr) + try userObject.set(DBUser.CodingKeys.cImageURLStrs.rawValue, value: user.cImageURLStrs) + try userObject.set(DBUser.CodingKeys.cAboutUS.rawValue, value: user.cAboutUS) + try userObject.set(DBUser.CodingKeys.cProvince.rawValue, value: user.cProvince) + try userObject.set(DBUser.CodingKeys.cCity.rawValue, value: user.cCity) + + try await userObject.save() + } + + func setTo(filed: String, _ value: Bool) async throws { + let query = LCQuery(className: className) + query.whereKey("id", .equalTo(AuthManager.shared.getUserID())) + let userObject = try await query.getFirst() + try userObject.set(filed, value: value) + try await userObject.save() + } + + func removeUser(id: String) async { + let query = LCQuery(className: className) + query.whereKey("id", .equalTo(id)) + guard let userObject = try? await query.getFirst() else { return } + try? await userObject.delete() + } +} diff --git a/IOS_study/Model/DBUser.swift b/IOS_study/Model/DBUser.swift new file mode 100644 index 0000000..18653a6 --- /dev/null +++ b/IOS_study/Model/DBUser.swift @@ -0,0 +1,90 @@ +// +// DBUser.swift +// IOS_study +// +// Created by CC-star on 2025/7/14. +// + +import Foundation + +struct DBUser: Codable, Identifiable {//用户相关 + var id = UUID().uuidString//id + var nickName = ""//昵称 + + var cName = ""//公式名字 + var cLogoURLStr = ""//公司logo + var cImageURLStrs: [String] = []//公司相关图片 + var cAboutUS = ""//公式简介 + var cProvince = "重庆"//省份 + var cCity = ""//城市 + var cIsUploaded: Bool { !cName.isEmpty }//公司信息是否上传 + + var hasPLJobs = false//长期兼职 + var hasReviews = false//点评 + + enum CodingKeys: String,CodingKey { + case id + case nickName + case cName + case cLogoURLStr + case cImageURLStrs + case cAboutUS + case cProvince + case cCity + case hasPLJobs + case hasReviews + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.id, forKey: .id) + try container.encode(self.nickName, forKey: .nickName) + try container.encode(self.cName, forKey: .cName) + try container.encode(self.cLogoURLStr, forKey: .cLogoURLStr) + try container.encode(self.cImageURLStrs, forKey: .cImageURLStrs) + try container.encode(self.cAboutUS, forKey: .cAboutUS) + try container.encode(self.cProvince, forKey: .cProvince) + try container.encode(self.cCity, forKey: .cCity) + try container.encode(self.hasPLJobs, forKey: .hasPLJobs) + try container.encode(self.hasReviews, forKey: .hasReviews) + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(String.self, forKey: .id) + self.nickName = try container.decodeIfPresent(String.self, forKey: .nickName) ?? "" + self.cName = try container.decodeIfPresent(String.self, forKey: .cName) ?? "" + self.cLogoURLStr = try container.decodeIfPresent(String.self, forKey: .cLogoURLStr) ?? "" + self.cImageURLStrs = try container.decodeIfPresent([String].self, forKey: .cImageURLStrs) ?? [] + self.cAboutUS = try container.decodeIfPresent(String.self, forKey: .cAboutUS) ?? "" + self.cProvince = try container.decodeIfPresent(String.self, forKey: .cProvince) ?? "重庆" + self.cCity = try container.decodeIfPresent(String.self, forKey: .cCity) ?? "" + self.hasPLJobs = try container.decodeIfPresent(Bool.self, forKey: .hasPLJobs) ?? false + self.hasReviews = try container.decodeIfPresent(Bool.self, forKey: .hasReviews) ?? false + } + + //先注释上面的init,在输入init生成下面的 + init(id: String = UUID().uuidString, nickName: String = "", cName: String = "", cLogoURLStr: String = "", cImageURLStrs: [String] = [], cAboutUS: String = "", cProvince: String = "重庆", cCity: String = "", hasPLJobs: Bool = false, hasReviews: Bool = false) { + self.id = id + self.nickName = nickName + self.cName = cName + self.cLogoURLStr = cLogoURLStr + self.cImageURLStrs = cImageURLStrs + self.cAboutUS = cAboutUS + self.cProvince = cProvince + self.cCity = cCity + self.hasPLJobs = hasPLJobs + self.hasReviews = hasReviews + } + +} + +//struct Company: Codable {//公司相关 +// var name = ""//公式名字 +// var logoURLStr = ""//公司logo +// var imageURLStrs: [String] = []//公司相关图片 +// var aboutUS = ""//公式简介 +// var province = "重庆"//省份 +// var city = ""//城市 +// var isUploaded: Bool { !name.isEmpty }//公司信息是否上传 +//} diff --git a/IOS_study/Model/OF.swift b/IOS_study/Model/OF.swift new file mode 100644 index 0000000..4d9257e --- /dev/null +++ b/IOS_study/Model/OF.swift @@ -0,0 +1,25 @@ +// +// OF.swift +// IOS_study +// +// Created by CC-star on 2025/7/10. +// + +import Foundation + +struct PLFilter: Equatable {//Equatable表示可以进行比较 + var business: String? + var city: String? + var totalCount:Int { + var count = 0 + if business != nil { count += 1 } + if city != nil { count += 1 } + return count + } +} + +enum PLOrder: String, CaseIterable, Identifiable { + var id: String { self.rawValue } + case updatedAt = "按更新时间" + case hourlywage = "按时给" +} diff --git a/IOS_study/Model/PLJob.swift b/IOS_study/Model/PLJob.swift new file mode 100644 index 0000000..ec4f185 --- /dev/null +++ b/IOS_study/Model/PLJob.swift @@ -0,0 +1,217 @@ +// +// PLJob.swift +// IOS_study +// +// Created by CC-star on 2025/6/28. +// + +import Foundation +//添加或修改属性之后-->重新生成codingkeys(加上String),修改decode,注释decode,重新生成init,打开decode注释 +struct PLJob: Identifiable, Codable {//遵循莫个协议,Codable:编/解码合体 + var id = UUID().uuidString//id + var isActive = true//是否上线 + var title = ""//标题 + var business = "互联网"//行业 + var tax = "详谈"//是否报税 + var hourlyWage = 100//时给 + var wantNum = 3//招几人 + var contactType = "微信"//联系方式类型 + var contact = ""//联系方式 + + //工作时间地点内容 + var startHour = 10//开始时间 + var startMin = 0//开始分钟 + var endHour = 20//结束时间 + var endMin = 0//结束分钟 + + //点击状态 + var has2 = false//是否第二个时间 + //第二个时间 + var startHour2 = 10//开始时间 + var startMin2 = 0//开始分钟 + var endHour2 = 20//结束时间 + var endMin2 = 0//结束分钟 + + var otherTime = ""//其他时间 + + var placeName = ""//完整地址 + var province = "北京"//省份 + var city = ""//城市 + var workContent = ""//工作内容 + + //工作要求 + var needExp = "1-3年"//经验 + var needEdu = "本科"//学历 + var needLan = "Swift"//语言 + var needFrame = "SwiftUI"//框架 + var otherNeed = ""//其他要求 + + //福利 + var moveFee = true//交通费 + var noOverTime = true//不加班 + var otherBenefit = ""//其他福利 + + + //其他 + var companyName = ""//公式名 + var userID = ""//用户id + //var creatTime = "" + var updatedAt = Date()//更新时间 + var wantWorkNum = 0//几人想去 + + enum CodingKeys: String,CodingKey { + case id + case isActive + case title + case business + case tax + case hourlyWage + case wantNum + case contactType + case contact + case startHour + case startMin + case endHour + case endMin + case has2 + case startHour2 + case startMin2 + case endHour2 + case endMin2 + case otherTime + case placeName + case province + case city + case workContent + case needExp + case needEdu + case needLan + case needFrame + case otherNeed + case moveFee + case noOverTime + case otherBenefit + case companyName + case userID + case updatedAt + case wantWorkNum + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(String.self, forKey: .id) + self.isActive = try container.decodeIfPresent(Bool.self, forKey: .isActive) ?? true + self.title = try container.decodeIfPresent(String.self, forKey: .title) ?? "" + self.business = try container.decodeIfPresent(String.self, forKey: .business) ?? "互联网" + self.tax = try container.decodeIfPresent(String.self, forKey: .tax) ?? "详谈" + self.hourlyWage = try container.decodeIfPresent(Int.self, forKey: .hourlyWage) ?? 100 + self.wantNum = try container.decodeIfPresent(Int.self, forKey: .wantNum) ?? 3 + self.contactType = try container.decodeIfPresent(String.self, forKey: .contactType) ?? "微信" + self.contact = try container.decodeIfPresent(String.self, forKey: .contact) ?? "" + self.startHour = try container.decodeIfPresent(Int.self, forKey: .startHour) ?? 10 + self.startMin = try container.decodeIfPresent(Int.self, forKey: .startMin) ?? 0 + self.endHour = try container.decodeIfPresent(Int.self, forKey: .endHour) ?? 20 + self.endMin = try container.decodeIfPresent(Int.self, forKey: .endMin) ?? 0 + self.has2 = try container.decodeIfPresent(Bool.self, forKey: .has2) ?? false + self.startHour2 = try container.decodeIfPresent(Int.self, forKey: .startHour2) ?? 10 + self.startMin2 = try container.decodeIfPresent(Int.self, forKey: .startMin2) ?? 0 + self.endHour2 = try container.decodeIfPresent(Int.self, forKey: .endHour2) ?? 20 + self.endMin2 = try container.decodeIfPresent(Int.self, forKey: .endMin2) ?? 0 + self.otherTime = try container.decodeIfPresent(String.self, forKey: .otherTime) ?? "" + self.placeName = try container.decodeIfPresent(String.self, forKey: .placeName) ?? "" + self.province = try container.decodeIfPresent(String.self, forKey: .province) ?? "浙江" + self.city = try container.decodeIfPresent(String.self, forKey: .city) ?? "" + self.workContent = try container.decodeIfPresent(String.self, forKey: .workContent) ?? "" + self.needExp = try container.decodeIfPresent(String.self, forKey: .needExp) ?? "1-3年" + self.needEdu = try container.decodeIfPresent(String.self, forKey: .needEdu) ?? "本科" + self.needLan = try container.decodeIfPresent(String.self, forKey: .needLan) ?? "Swift" + self.needFrame = try container.decodeIfPresent(String.self, forKey: .needFrame) ?? "SwiftUI" + self.otherNeed = try container.decodeIfPresent(String.self, forKey: .otherNeed) ?? "" + self.moveFee = try container.decodeIfPresent(Bool.self, forKey: .moveFee) ?? true + self.noOverTime = try container.decodeIfPresent(Bool.self, forKey: .noOverTime) ?? true + self.otherBenefit = try container.decodeIfPresent(String.self, forKey: .otherBenefit) ?? "" + self.companyName = try container.decodeIfPresent(String.self, forKey: .companyName) ?? "" + self.userID = try container.decodeIfPresent(String.self, forKey: .userID) ?? "" + self.updatedAt = try container.decodeIfPresent(Date.self, forKey: .updatedAt) ?? Date() + self.wantWorkNum = try container.decodeIfPresent(Int.self, forKey: .wantWorkNum) ?? 0 + } + +// func encode(to encoder: any Encoder) throws { +// var container = encoder.container(keyedBy: CodingKeys.self) +// try container.encode(self.id, forKey: .id) +// try container.encode(self.isActive, forKey: .isActive) +// try container.encode(self.title, forKey: .title) +// try container.encode(self.business, forKey: .business) +// try container.encode(self.tax, forKey: .tax) +// try container.encode(self.hourlyWage, forKey: .hourlyWage) +// try container.encode(self.wantNum, forKey: .wantNum) +// try container.encode(self.contactType, forKey: .contactType) +// try container.encode(self.contact, forKey: .contact) +// try container.encode(self.startHour, forKey: .startHour) +// try container.encode(self.startMin, forKey: .startMin) +// try container.encode(self.endHour, forKey: .endHour) +// try container.encode(self.endMin, forKey: .endMin) +// try container.encode(self.has2, forKey: .has2) +// try container.encode(self.startHour2, forKey: .startHour2) +// try container.encode(self.startMin2, forKey: .startMin2) +// try container.encode(self.endHour2, forKey: .endHour2) +// try container.encode(self.endMin2, forKey: .endMin2) +// try container.encode(self.otherTime, forKey: .otherTime) +// try container.encode(self.placeName, forKey: .placeName) +// try container.encode(self.province, forKey: .province) +// try container.encode(self.city, forKey: .city) +// try container.encode(self.workContent, forKey: .workContent) +// try container.encode(self.needExp, forKey: .needExp) +// try container.encode(self.needEdu, forKey: .needEdu) +// try container.encode(self.needLan, forKey: .needLan) +// try container.encode(self.needFrame, forKey: .needFrame) +// try container.encode(self.otherNeed, forKey: .otherNeed) +// try container.encode(self.moveFee, forKey: .moveFee) +// try container.encode(self.noOverTime, forKey: .noOverTime) +// try container.encode(self.otherBenefit, forKey: .otherBenefit) +// try container.encode(self.companyName, forKey: .companyName) +// try container.encode(self.userID, forKey: .userID) +// try container.encode(self.updatedAt, forKey: .updatedAt) +// try container.encode(self.wantWorkNum, forKey: .wantWorkNum) +// } + + init(id: String = UUID().uuidString, isActive: Bool = true, title: String = "", business: String = "互联网", tax: String = "详谈", hourlyWage: Int = 100, wantNum: Int = 3, contactType: String = "微信", contact: String = "", startHour: Int = 10, startMin: Int = 0, endHour: Int = 20, endMin: Int = 0, has2: Bool = false, startHour2: Int = 10, startMin2: Int = 0, endHour2: Int = 20, endMin2: Int = 0, otherTime: String = "", placeName: String = "", province: String = "浙江", city: String = "", workContent: String = "", needExp: String = "1-3年", needEdu: String = "本科", needLan: String = "Swift", needFrame: String = "SwiftUI", otherNeed: String = "", moveFee: Bool = true, noOverTime: Bool = true, otherBenefit: String = "", companyName: String = "", userID: String = "", updatedAt: Date = Date(), wantWorkNum: Int = 0) { + self.id = id + self.isActive = isActive + self.title = title + self.business = business + self.tax = tax + self.hourlyWage = hourlyWage + self.wantNum = wantNum + self.contactType = contactType + self.contact = contact + self.startHour = startHour + self.startMin = startMin + self.endHour = endHour + self.endMin = endMin + self.has2 = has2 + self.startHour2 = startHour2 + self.startMin2 = startMin2 + self.endHour2 = endHour2 + self.endMin2 = endMin2 + self.otherTime = otherTime + self.placeName = placeName + self.province = province + self.city = city + self.workContent = workContent + self.needExp = needExp + self.needEdu = needEdu + self.needLan = needLan + self.needFrame = needFrame + self.otherNeed = otherNeed + self.moveFee = moveFee + self.noOverTime = noOverTime + self.otherBenefit = otherBenefit + self.companyName = companyName + self.userID = userID + self.updatedAt = updatedAt + self.wantWorkNum = wantWorkNum + } + + static let job = PLJob(title: "iOS程序员", tax: "报税", contact: "12345", has2: true, startHour2: 14, endHour2: 22, otherTime: "可以根据实际情况调整", placeName: "阿里巴巴西溪园区", province: "浙江", city: "杭州", workContent: "iOS开发啊啊iOS开发啊啊iOS开发啊啊iOS开发啊啊iOS开发啊啊iOS开发啊啊", otherNeed: "什么都不需要啊啊什么都不需要啊啊什么都不需要啊啊什么都不需要啊啊", otherBenefit: "什么都有啊啊什么都有啊啊什么都有啊啊什么都有啊啊什么都有啊啊",companyName:"阿里巴巴") +} diff --git a/IOS_study/Model/Report.swift b/IOS_study/Model/Report.swift new file mode 100644 index 0000000..1249810 --- /dev/null +++ b/IOS_study/Model/Report.swift @@ -0,0 +1,31 @@ +// +// Report.swift +// IOS_study +// +// Created by CC-star on 2025/7/24. +// + +import Foundation + +struct JobReport: Codable, Identifiable { + var id = UUID().uuidString + //被举报的工作的信息 + var jobID = "" + var jobType = "" + var jobTitle = "" + var bossID = "" + //举报者 + var userID = "" +// var userCompanyName = "" + + + enum CodingKeys: String, CodingKey { + case id + case jobType + case jobID + case jobTitle + case bossID + case userID + } + +} diff --git a/IOS_study/Model/Review.swift b/IOS_study/Model/Review.swift new file mode 100644 index 0000000..75a8eec --- /dev/null +++ b/IOS_study/Model/Review.swift @@ -0,0 +1,18 @@ +// +// Review.swift +// IOS_study +// +// Created by CC-star on 2025/7/25. +// + +import Foundation + +struct Review: Identifiable, Codable { + var id = UUID().uuidString + var userAvatar = "0"//头像 + var userNickName = ""//昵称 + var reviewType = "未面试"//点评类型 + var goods: [String] = []//好评 + var bads: [String] = []//差评 + var content = "" +} diff --git a/IOS_study/Post/CompanyGalleryORView.swift b/IOS_study/Post/CompanyGalleryORView.swift new file mode 100644 index 0000000..f561d55 --- /dev/null +++ b/IOS_study/Post/CompanyGalleryORView.swift @@ -0,0 +1,50 @@ +// +// CompanyGalleryORView.swift +// IOS_study +// +// Created by CC-star on 2025/7/22. +// + +import SwiftUI +import Kingfisher + +struct CompanyGalleryORView: View { + let imageURLStrs: [String] + @Environment(\.dismiss) var dismiss + @State var currentPage: Int + @State var dragOffset: CGSize = .zero + var body: some View { + TabView(selection: $currentPage) { + ForEach(imageURLStrs.indices, id: \.self) { i in + VStack { + KFImage(URL(string: imageURLStrs[i])) + .placeholder { ProgressView() }.loadDiskFileSynchronously().fade(duration: 0.15) + .resizable().scaledToFit().tag(i) + .offset(y: dragOffset.height) + }.frame(maxWidth: .infinity, maxHeight: .infinity).pinchToZoom() + } + }.bg().tabViewStyle(.page) + .toolbarVisibility(.hidden, for: .tabBar) + .toolbar { + ToolbarItem(placement: .principal) { + Text("\(currentPage + 1)/\(imageURLStrs.count)").tc().bold() + } + } + .gesture(//手势【拖拽】 + DragGesture() + .onChanged({ gesture in + if gesture.translation.height > 0 { + dragOffset = gesture.translation + } + }) + .onEnded { gesture in + if gesture.translation.height > 80 {//垂直偏移80 -> 返回上一页 + dismiss() + } else { + withAnimation { dragOffset = .zero } + } + } + ) + } +} + diff --git a/IOS_study/Post/CompanyGalleryView.swift b/IOS_study/Post/CompanyGalleryView.swift new file mode 100644 index 0000000..b77ee8c --- /dev/null +++ b/IOS_study/Post/CompanyGalleryView.swift @@ -0,0 +1,46 @@ +// +// CompanyGalleryView.swift +// IOS_study +// +// Created by CC-star on 2025/7/22. +// + +import SwiftUI + +struct CompanyGalleryView: View { + @Environment(ProfileViewModel.self) var vm + @Environment(\.dismiss) var dismiss + @State var currentPage: Int + @State var showConfirm = false + var body: some View { + TabView(selection: $currentPage) { + ForEach(vm.companyUIImages.indices, id: \.self) { i in + Image(uiImage: vm.companyUIImages[i]).resizable().scaledToFit().tag(i) + } + }.bg().tabViewStyle(.page) + .toolbar { + ToolbarItem(placement: .principal) { + Text("\(currentPage + 1)/\(vm.companyUIImages.count)").tc().bold() + } + ToolbarItem(placement: .topBarTrailing) { + Image(systemName: "trash").onTapGesture { + showConfirm = true + } + } + } + .confirmationDialog("删除一张图片", isPresented: $showConfirm, actions: { + Button { + guard currentPage < vm.companyUIImages.count else { return } + vm.companyUIImages.remove(at: currentPage) + if vm.companyUIImages.isEmpty { + dismiss() + } else if currentPage >= vm.companyUIImages.count { + currentPage = vm.companyUIImages.count - 1 + } + } label: { + Text("删除") + } + }, message: { Text("要删除这张图片吗?") }) + } +} + diff --git a/IOS_study/Post/PostCompanyView.swift b/IOS_study/Post/PostCompanyView.swift new file mode 100644 index 0000000..92ad957 --- /dev/null +++ b/IOS_study/Post/PostCompanyView.swift @@ -0,0 +1,152 @@ +// +// PostCompanyView.swift +// IOS_study +// +// Created by CC-star on 2025/7/15. +// + +import SwiftUI +import PhotosUI + +struct PostCompanyView: View { + @State var isEditing = false//是否在编辑 + @Environment(HUD.self) var hud//提示信息功能 + @Environment(ProfileViewModel.self) var ProfileVM + @Binding var naviPath: NavigationPath + var draftUser: DBUser { ProfileVM.draftUser } + var invalidMsg: String? { + if draftUser.cName.isBlank { return "公司名字没有写" } + if draftUser.cProvince.isBlank { return "公司所在省份没有写" } + if draftUser.cCity.isBlank { return "公司所在城市没有写" } + if draftUser.cName.count > 50 { return "公司名字不能超过50字" } + if draftUser.cCity.count > 50 { return "公司所在城市名不能超过50字" } + if draftUser.cAboutUS.count > 20 { return "公司简介不能超过20字" } + return nil + } + + + var body: some View { + @Bindable var vm = ProfileVM + ScrollView { + VStack(alignment: .leading, spacing: 14) { + HStack { + Text("公司名称:").littleTStyle() + TextField("请输入完整公司名称", text: $vm.draftUser.cName, axis: .vertical) + .tfStyle()//单行 + } + HStack { + Text("公司地址:").littleTStyle() + Picker("省份", selection: $vm.draftUser.cProvince) { + ForEach(kProvinceArr, id: \.self) { Text($0) } + }.pStyle() + TextField("城市",text: $vm.draftUser.cCity).tfStyle() + } + VStack(alignment: .leading, spacing: 9) { + Text("公司简介(选填)").littleTStyle() + TextField("", text: $vm.draftUser.cAboutUS).tfStyleMultiL() + } + VStack(alignment: .leading) { + HStack { + Text("公司Logo(选填)").littleTStyle() + Text(vm.logoImageErrorMsg).size15pb() + } + PhotosPicker(selection: $vm.logoItem, matching: .images, photoLibrary: .shared()) { + if let uiImage = vm.logoUIImage { + Image(uiImage: uiImage) + .resizable()//可改变大小 + // .scaledToFit()//保持宽高比不变,显示完整图片 + .scaledToFill()//保持宽高比不变,充满空间,会有裁剪 + .thumbFrame().radius() + } else { + addImageBtn + } + } + } + VStack(alignment: .leading) { + HStack { + Text("公司图片(选填)").littleTStyle() + Text(vm.companyUIImagesErrorMsg).size15pb() + } + ScrollView(.horizontal) { + HStack { + if vm.isLoadingCompanyImages { + ProgressView().thumbFrame() + } else { + ForEach(vm.companyUIImages.indices, id: \.self) { i in + NavigationLink { + CompanyGalleryView(currentPage: i) + } label: { + Image(uiImage: vm.companyUIImages[i]).resizable().scaledToFill().thumbFrame().radius() + }.disabled(!vm.isCompanyImagesLoaded) + } + } + if vm.companyUIImages.count < 6 { + PhotosPicker(selection: $vm.companyItems, maxSelectionCount: 6 - vm.companyUIImages.count, matching: .images, photoLibrary: .shared()) { addImageBtn } + } + } + }.scrollIndicators(.hidden) + } + + Button { + // hud.show("提交成功") + Task {//开启异步任务 + await postCompany() + } + } label: { + if vm.isPostingCompany { + ProgressView().appleStyleP()//提交加载 + }else { + Text("提交").appleStyle() + } + }.disabled(vm.isPostingCompany) + + }.padding() + postBottomEView() + }.scrollIndicators(.hidden).tc().size17().bg() + .navigationBarBackButtonHidden().navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { Text("公司信息").tc().bold() } + ToolbarItem(placement: .topBarLeading) { + Button { + if !naviPath.isEmpty {//检查是否为最后一页 + naviPath.removeLast()//返回上一页 + } + } label: { + Image(systemName: "chevron.left").secondary().padding(.horizontal,6) + } + } + } + .toolbarVisibility(.hidden, for: .tabBar) + .onAppear { + if ProfileVM.isCompanyImagesLoaded { return } + Task { + await vm.loadCompanyLogoAndIamges() + } + } + } + + var addImageBtn: some View { + Image(systemName: "plus").font(.title).secondary().thumbFrame() + .border(borderColor: Color(.systemGray4)) + } + func postCompany() async { + if let invalidMsg = invalidMsg {//也可以写成if let invalidMsg + hud.show(invalidMsg) + return + } + ProfileVM.isPostingCompany = true + defer { ProfileVM.isPostingCompany = false } + UIApplication.shared.hideKeyboard()//收起键盘 + + do { + try await ProfileVM.postCompany() + hud.show("发布成功") + if !naviPath.isEmpty { naviPath.removeLast() } + } catch { + print("上传或修改公司信息失败:\(error)") + hud.show("上传或修改公司信息失败:\(error)") + } + + } +} + diff --git a/IOS_study/Post/PostJobView.swift b/IOS_study/Post/PostJobView.swift new file mode 100644 index 0000000..5f5d3a0 --- /dev/null +++ b/IOS_study/Post/PostJobView.swift @@ -0,0 +1,27 @@ +// +// PostJobView.swift +// IOS_study +// +// Created by CC-star on 2025/6/28. +// + +import SwiftUI + +struct PostJobView: View { + @Environment(AuthViewModel.self) var authVM + @Environment(TabBarViewModel.self) var TabBarVM + @State var naviPath = NavigationPath() + var body: some View { + if authVM.isLogin { + NavigationStack(path: $naviPath) { + PostPLJobIView(naviPath: $naviPath) + } + } else { + Button { + TabBarVM.selectedTab = "profile" + } label: { + Text("去登录").appleStyle().padding() + } + } + } +} diff --git a/IOS_study/Post/PostPLJobIView+.swift b/IOS_study/Post/PostPLJobIView+.swift new file mode 100644 index 0000000..540b300 --- /dev/null +++ b/IOS_study/Post/PostPLJobIView+.swift @@ -0,0 +1,69 @@ +// +// PostPLJobIView+.swift +// IOS_study +// +// Created by CC-star on 2025/6/29. +// + +import SwiftUI + +extension PostPLJobIView { + + var postBtn: some View { + Button { + // hud.show("提交成功") + Task {//开启异步任务 + await postJob() + } + } label: { + if vm.isPosting { + ProgressView().appleStyleP()//提交加载 + }else { + Text("提交").appleStyle() + } + }.disabled(vm.isPosting).padding().bg() + } + + func postJob() async { + if let invalidMsg = invalidMsg {//也可以写成if let invalidMsg + hud.show(invalidMsg) + return + } + // print("输入ok") + vm.isPosting = true + defer { + vm.isPosting = false + } + + UIApplication.shared.hideKeyboard()//收起键盘 + + workPlaceName = draftJob.placeName + workProvince = draftJob.province + workCity = draftJob.city + workContact = draftJob.contact + + vm.draftJob.companyName = profileVM.user.cName + + do { + try await vm.postJob(isEditing: isEditing) + if !profileVM.user.hasPLJobs { + profileVM.user.hasPLJobs = true + try await UserManager.shared.setTo(filed: DBUser.CodingKeys.hasPLJobs.rawValue, true) + } + profileVM.shouldRefreshMyPLJobs = true + if isEditing { + hud.show("修改成功") + if !naviPath.isEmpty { naviPath.removeLast() } + } else { + hud.show("发布成功") + tabbarVM.goJobsAndRefresh("兼职") + } + scrollView?.scrollTo("title",anchor: .top) + vm.draftJob = PLJob() + } catch { + hud.show("\(error)") + print("用户上传或修改兼职失败:\(error)") + } + // vm.isPosting = false + } +} diff --git a/IOS_study/Post/PostPLJobIView.swift b/IOS_study/Post/PostPLJobIView.swift new file mode 100644 index 0000000..a5bfef3 --- /dev/null +++ b/IOS_study/Post/PostPLJobIView.swift @@ -0,0 +1,320 @@ +// +// PostPLJobIView.swift +// IOS_study +// +// Created by CC-star on 2025/6/28. +// + +import SwiftUI + +struct PostPLJobIView: View { + var isEditing = false //默认新增工作 + + @AppStorage("workPlaceName") var workPlaceName = "" + @AppStorage("workProvince") var workProvince = "" + @AppStorage("workCity") var workCity = "" + @AppStorage("workContact") var workContact = "" + @Environment(PLJobViewModel.self) var vm + @Environment(ProfileViewModel.self) var profileVM + @Environment(HUD.self) var hud + @Environment(TabBarViewModel.self) var tabbarVM + @State var scrollView: ScrollViewProxy?// = nil + @Binding var naviPath: NavigationPath + @State var WordNum20 = 20 + @State var WordNum50 = 50 + @State var WordNum100 = 100 + @State var WordNum200 = 200 + @State var WordNum300 = 300 + @State var WordNum500 = 500 + + var draftJob: PLJob{ vm.draftJob } + + var invalidMsg: String? { + if !profileVM.user.cIsUploaded { return "需要先上传公司信息" } + if draftJob.title.isBlank { return "工作名称未写" } + if draftJob.hourlyWage < 20 || draftJob.hourlyWage > 1000 { return "基本时给太低或太高" } + if draftJob.placeName.isBlank { return "工作地点未填" } + if draftJob.city.isBlank { return "城市未填" } + if draftJob.contact.isBlank { return "联系方式未填"} + if draftJob.title.count > WordNum50 { return "工作名称不能超过\($WordNum50)" } + if draftJob.otherTime.count > WordNum100 { return "时间说明不能超过\($WordNum100)" } + if draftJob.otherNeed.count > WordNum300 { return "工作要求不能超过\($WordNum300)" } + if draftJob.workContent.count > WordNum500 { return "工作内容不能超过\($WordNum500)" } + if draftJob.otherBenefit.count > WordNum300 { return "工作福利不能超过\($WordNum300)" } + if draftJob.placeName.count > WordNum50 { return "工作地点名称不能超过\($WordNum50)" } + if draftJob.city.count > WordNum20 { return "工作地城市不能超过\($WordNum20)" } + if draftJob.contact.count > WordNum200 { return "联系方式不能超过\($WordNum200)" } + return nil + } + + var body: some View { + @Bindable var vm = vm + ScrollViewReader { scrollView in + ScrollView(showsIndicators: false) { + VStack(spacing: kStackSpacingS) { + VStack { + if profileVM.user.cIsUploaded { + Text(profileVM.user.cName).subTStyleL() + } else { + Text("请先至个人页添加公司").size15().red() + } + }.push(to: .center).padding().bg() + // MARK: - 基本信息 + VStack(alignment: .leading,spacing: kStackSpacing) { + HStack { + VStack(alignment: .leading) { + Text("工作业种").littleTStyle() + Picker("工作业种", selection: $vm.draftJob.business) { + ForEach(kBusinessArr, id: \.self){ + Text($0) + } + }.pStyle() + } + VStack(alignment: .leading) { + Text("工作名称").littleTStyle() + TextField("IOS程序员", text: $vm.draftJob.title).tfStyle() + } + }.id("title") + VStack(alignment: .leading) { + Text("是否报税").littleTStyle() + Picker("是否报税", selection: $vm.draftJob.tax){ + ForEach(kTaxArr, id: \.self){ + Text($0) + } + }.pickerStyle(.segmented) + }.padding(.bottom,5) + HStack { + Text("基本时给:").littleTStyle() + TextField("", value: $vm.draftJob.hourlyWage, formatter: NumberFormatter()) + .size18ab().multilineTextAlignment(.center).kerning(2) + .keyboardType(.numberPad).tfStyle().frame(width: 88) + Text("请填写最低时给").size13s() + } + HStack { + Text("招聘人数:").littleTStyle() + Picker("招聘人数", selection: $vm.draftJob.wantNum) { ForEach(kWantNumArr, id: \.self){ + Text("\($0)人")//字符串插值 + } + }.pStyle() + Text("请填写最多招聘人数").size13s() + } + + }.padding().bg() + // MARK: - 工作时间 + VStack(alignment: .leading,spacing: kStackSpacingS) { + Text("工作时间").littleTStyle() + HStack { + HStack(spacing: 3) { + Picker("起始小时", selection: $vm.draftJob.startHour) { ForEach(0..<25) {Text("\($0)")} + }.pStyle() + Text(":").conlonStyle() + Picker("起始分钟", selection: $vm.draftJob.startMin) { ForEach([0,30], id: \.self) {Text($0 == 0 ? "00":"\($0)")} + }.pStyle() + + Image(systemName: "minus") + + Picker("结束小时", selection: $vm.draftJob.endHour) { ForEach(0..<25) {Text("\($0)")} + }.pStyle() + Text(":").conlonStyle() + Picker("结束分钟", selection: $vm.draftJob.endMin) { ForEach([0,30], id: \.self) {Text($0 == 0 ? "00":"\($0)")} + }.pStyle() + } + Button { + withAnimation { + vm.draftJob.has2.toggle() + } + } label: { + Image(systemName: vm.draftJob.has2 ? "minus.circle.fill" : "plus.circle.fill") + .foregroundStyle(vm.draftJob.has2 ? .red : .accent) + }.font(.title2).padding(.horizontal,3) + } + + if(vm.draftJob.has2){ + HStack(spacing: 3) { + Picker("第二个起始小时", selection: $vm.draftJob.startHour2) { ForEach(0..<25) {Text("\($0)")} + }.pStyle() + Text(":").conlonStyle() + Picker("第二个起始分钟2", selection: $vm.draftJob.startMin2) { ForEach([0,30], id: \.self) {Text($0 == 0 ? "00":"\($0)")} + }.pStyle() + + Image(systemName: "minus") + + Picker("第二个结束小时2", selection: $vm.draftJob.endHour2) { ForEach(0..<25) {Text("\($0)")} + }.pStyle() + Text(":").conlonStyle() + Picker("第二个结束分钟2", selection: $vm.draftJob.endMin2) { ForEach([0,30], id: \.self) {Text($0 == 0 ? "00":"\($0)")} + }.pStyle() + } + } + + Text("追加说明").littleTStyle() + TextField("具体时间段可以详谈", text: $vm.draftJob.otherTime, axis: .vertical).tfStyleMultiS() + }.push(to: .leading).padding().bg() + // MARK: - 工作要求 + VStack(alignment: .leading,spacing: kStackSpacing) { + HStack { + VStack(alignment: .leading) { + Text("经验要求").littleTStyle() + Picker("经验要求", selection: $vm.draftJob.needExp) { ForEach(kNeedExpArr, id: \.self) {Text($0)} + }.pStyle() + } + VStack(alignment: .leading) { + Text("学历要求").littleTStyle() + Picker("学历要求", selection: $vm.draftJob.needEdu) { ForEach(kNeedEduArr, id: \.self) {Text($0)} + }.pStyle() + } + } + HStack { + VStack(alignment: .leading) { + Text("语言要求").littleTStyle() + Picker("学历要求", selection: $vm.draftJob.needLan) { ForEach(kNeedLanArr, id: \.self) {Text($0)} + }.pStyle() + } + VStack(alignment: .leading) { + Text("框架要求").littleTStyle() + Picker("学历要求", selection: $vm.draftJob.needFrame) { ForEach(kNeedFrameArr, id: \.self) {Text($0)} + }.pStyle() + } + } + VStack(alignment: .leading) { + Text("更多要求/加分项(选填)").littleTStyle() + TextField("", text: $vm.draftJob.otherNeed, axis: .vertical).tfStyleMultiM() + } + }.padding().bg() + // MARK: - 工作内容 + VStack(alignment: .leading) { + Text("工作内容(选填)").littleTStyle() + TextField("", text: $vm.draftJob.workContent, axis: .vertical).tfStyleMultiM() + + }.padding().bg() + // MARK: - 公司福利 + VStack(alignment: .leading,spacing: kStackSpacing) { + Text("公司福利").littleTStyle() + HStack { + Text("交通费报销").onTapGesture { + vm.draftJob.moveFee.toggle() + }.toggleStyle(isTap: vm.draftJob.moveFee) + Text("不加班").onTapGesture { + vm.draftJob.noOverTime.toggle() + }.toggleStyle(isTap: vm.draftJob.noOverTime) + } + VStack(alignment: .leading) { + Text("更多福利(选填)").littleTStyle() + TextField("", text: $vm.draftJob.otherBenefit, axis: .vertical).tfStyleMultiL() + } + }.padding().bg() + // MARK: - 工作地点 + VStack(alignment: .leading,spacing: kStackSpacing) { + HStack { + Text("省份").littleTStyle() + Picker("省份", selection: $vm.draftJob.province) { ForEach(kProvinceArr, id: \.self) {Text($0)} + }.pStyle() + if !workProvince.isEmpty { + HStack(spacing: 3) { + Text("上次填的:").size13s() + Button { + vm.draftJob.province = workProvince + } label: { + Text(workProvince).labelBG() + } + } + } + + } + HStack { + Text("城市").littleTStyle() + TextField("", text: $vm.draftJob.city, axis: .vertical).tfStyle().frame(width: 115) + if !workCity.isEmpty { + HStack(spacing: 3) { + Text("上次填的:").size13s() + Button { + vm.draftJob.city = workCity + } label: { + Text(workCity).labelBG() + } + } + } + } + VStack(alignment: .leading) { + Text("工作地点").littleTStyle() + TextField("请填写完整工作地址", text: $vm.draftJob.placeName, axis: .vertical).tfStyle() + + if !workPlaceName.isEmpty { + HStack(spacing: 3) { + Text("上次填的:").size13s() + Button { + vm.draftJob.placeName = workPlaceName + } label: { + Text(workPlaceName).labelBG() + } + } + } + } + // Button { + // workProvince = province + // workCity = city + // workProvince = province + // } label: { + // Text("存本地") + // }.btnStyle() + // + }.padding().bg() + // MARK: - 应聘方式 + VStack(alignment: .leading,spacing: kStackSpacing){ + Text("联系方式/应聘方式").littleTStyle() + HStack { + Picker("联系方式种类", selection: $vm.draftJob.contactType) { + ForEach(kContactTypeArr, id: \.self) {Text($0)} + }.pStyle() + TextField("", text: $vm.draftJob.contact, axis: .vertical).tfStyle() + } + if !workContact.isEmpty { + HStack(spacing: 3) { + Text("上次填的:").size13s() + Button { + vm.draftJob.contact = workContact + } label: { + Text(workContact).labelBG() + } + } + } + + Text("据调查,99%的人才都喜欢使用微信联系").size13().red() + }.padding().bg() + + // MARK: - 提交按钮 + postBtn +// +// withAnimation {//withAnimation -> 动画 +// Button("点击") { +// scrollView.scrollTo("title",anchor: .top)//返回到id为title的view的top(id为title的view的顶部) +// } +// } + + }.tc().bg2() + postBottomEView().bg() + }.navigationBarBackButtonHidden(isEditing).navigationBarTitleDisplayMode(.inline) + .toolbar { + if isEditing { + ToolbarItem(placement: .principal) { Text("编辑兼职").tc().bold() } + ToolbarItem(placement: .topBarLeading) { + Button { + if !naviPath.isEmpty {//检查是否为最后一页 + naviPath.removeLast()//返回上一页 + } + } label: { + Image(systemName: "chevron.left").secondary().padding(.horizontal,6) + } + } + } + } + .onAppear() { + self.scrollView = scrollView + } + } + } +} + +#Preview { + PostPLJobIView(naviPath: .constant(NavigationPath())) +} diff --git a/IOS_study/Post/PostReviewView.swift b/IOS_study/Post/PostReviewView.swift new file mode 100644 index 0000000..77cdcbc --- /dev/null +++ b/IOS_study/Post/PostReviewView.swift @@ -0,0 +1,18 @@ +// +// PostReviewView.swift +// IOS_study +// +// Created by CC-star on 2025/7/25. +// + +import SwiftUI + +struct PostReviewView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + PostReviewView() +} diff --git a/IOS_study/Preview Content/Preview Assets.xcassets/Contents.json b/IOS_study/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/IOS_study/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS_study/Profile/PLJobView+RevUI.swift b/IOS_study/Profile/PLJobView+RevUI.swift new file mode 100644 index 0000000..d7fab89 --- /dev/null +++ b/IOS_study/Profile/PLJobView+RevUI.swift @@ -0,0 +1,23 @@ +// +// PLJobView+RevUI.swift +// IOS_study +// +// Created by CC-star on 2025/7/25. +// + +import SwiftUI + +extension PLJobView { + var goRevireBtn: some View { + Button { + if authVM.isLogin { + reviewVM.drafReview = Review() + showPostReview = true + } else { + hud.show("请先登录") + } + } label: { + Text("对这个工作有看法?去点评").appleStyle() + }.buttonStyle(.borderless).cellStyle() + } +} diff --git a/IOS_study/Profile/ProfileView+ComUI.swift b/IOS_study/Profile/ProfileView+ComUI.swift new file mode 100644 index 0000000..1d740d2 --- /dev/null +++ b/IOS_study/Profile/ProfileView+ComUI.swift @@ -0,0 +1,98 @@ +// +// ProfileView+ComUI.swift +// IOS_study +// +// Created by CC-star on 2025/7/18. +// + +import SwiftUI +import Kingfisher + +extension ProfileView { + var myCompanyView: some View { + VStack(alignment: .leading, spacing: 14) { + HStack { + // MARK: - 公司头部信息 + Text("我的公司").headline().tc() + if user.cIsUploaded { + Button { + ProfileVM.draftUser = user + naviPath.append(Navi5.EditCompany) + } label: { Text("编辑") }.btnStyle() + } else { + Button { + ProfileVM.draftUser = DBUser() + naviPath.append(Navi5.AddCompany) + } label: { Text("去添加") }.btnStyle() + } + } + + if user.cIsUploaded {//上传了公司信息 + VStack(alignment: .leading, spacing: 9) { + HStack { + if user.cLogoURLStr.isEmpty { + Text("🏬").font(.system(size: 40)) + } else { + Button { + showLogo = true + } label: { + KFImage(URL(string: user.cLogoURLStr)) + .placeholder { ProgressView() }.loadDiskFileSynchronously().fade(duration: 0.15) + .onSuccess { result in + let image = result.image + logoAspectRatio = image.size.width / image.size.height + } + .onFailure { error in + print("图片加载失败:\(error)") + } + .resizable().scaledToFit().thumbFrameS(aspectRatio: logoAspectRatio).radius(6) + }.buttonStyle(.borderless) + } + + VStack(alignment: .leading, spacing: 5) { + Text(user.cName).subTStyle()//公司名字 + Text(user.cProvince + "-" + user.cCity) + } + } + }.textStyle() + + // MARK: - 公司简介 + if !user.cAboutUS.isEmpty { + VStack(alignment: .leading, spacing: 9) { + Text("公司简介").subTStyle() + if sizeClass == .compact { + ZStack(alignment: .bottomTrailing) { + Text(user.cAboutUS).allowMulti().lineLimit((user.cAboutUS.count < 88 || isExpand) ? nil : 4) + if user.cAboutUS.count > 88 { + Text(isExpand ? "收起" : "展开").size14().accent().padding([.leading, .top], 3).bg() + .onTapGesture { isExpand.toggle() } + } + } + } else { + Text(user.cAboutUS).allowMulti() + } + }.size15().tc2().lineSpacing(6) + } + // MARK: - 公司相关图片 + if !user.cImageURLStrs.isEmpty { + ScrollView(.horizontal) { + HStack { + ForEach(user.cImageURLStrs.indices, id: \.self) { i in + if i < user.cImageURLStrs.count { + NavigationLink { + CompanyGalleryORView(imageURLStrs: user.cImageURLStrs, currentPage: i) + } label: { + KFImage(URL(string: user.cImageURLStrs[i])) + .placeholder { ProgressView() }.loadDiskFileSynchronously().fade(duration: 0.15) + .resizable().scaledToFill().thumbFrame().radius(6) + } + } + } + } + + }.scrollIndicators(.hidden) + } + } + }.push(to: .leading).cellStyle() + } +} diff --git a/IOS_study/Profile/ProfileView+JobUI.swift b/IOS_study/Profile/ProfileView+JobUI.swift new file mode 100644 index 0000000..3c8354b --- /dev/null +++ b/IOS_study/Profile/ProfileView+JobUI.swift @@ -0,0 +1,56 @@ +// +// ProfileView+JobUI.swift +// IOS_study +// +// Created by CC-star on 2025/7/23. +// + +import SwiftUI + +extension ProfileView { + var myJobsView: some View { + VStack(alignment: .leading, spacing: kStackSpacing) { + let jobsCount = ProfileVM.pLJobsCount + let jobsCountText = jobsCount > 0 ? "(\(ProfileVM.aPLJobsCount)/\(ProfileVM.pLJobsCount))" : "" + Text("我发布的工作\(jobsCountText)").headline().tc() + if !ProfileVM.plJobs.isEmpty { + ForEach(ProfileVM.plJobs) { job in + Button { + plJobVM.job = job + naviPath.append(Navi5.PLJobsToPLJob) + } label: { + HStack { + VStack(alignment: .leading, spacing: kStackSpacingS) { + HStack { + Text("兼职").labelPL() + Text(job.title).oneLineStyleMiddle() + } + HStack { + Text(job.needExp).labelBG() + Text(job.province + "-" + job.city).labelBG() + if !job.isActive { + Text("已下线").labelTA() + } + } + }.push(to: .leading) + + Text("\(job.hourlyWage)").bold().accent() + } + }.buttonStyle(.borderless) + exDivider() + } + if !ProfileVM.isPLFinshed { + Button { + Task { + await ProfileVM.getPLJobs() + } + } label: { + if ProfileVM.isFcPLJobs { ProgressView().loadingJobStyleP() } else { Text("加载更多兼职职位").loadingJobStyle() } + }.buttonStyle(.borderless).padding(.top, -6).push(to: .center) + } + } else { + Text("暂无发布的工作").secondary() + } + }.push(to: .leading).textStyle().cellStyle() + } +} diff --git a/IOS_study/Profile/ProfileView.swift b/IOS_study/Profile/ProfileView.swift new file mode 100644 index 0000000..78f00bb --- /dev/null +++ b/IOS_study/Profile/ProfileView.swift @@ -0,0 +1,88 @@ +// +// ProfileView.swift +// IOS_study +// +// Created by CC-star on 2025/7/15. +// + +import SwiftUI +import Kingfisher + +struct ProfileView: View { + @Environment(HUD.self) var hud + @Environment(AuthViewModel.self) var authVM + @Environment(SignInPhoneViewModel.self) var SignInPhoneVM + @Environment(ProfileViewModel.self) var ProfileVM + @Environment(PLJobViewModel.self) var plJobVM + @Environment(\.horizontalSizeClass) var sizeClass + @State var naviPath = NavigationPath() + @State var logoAspectRatio: CGFloat = 1 + @State var isExpand = false + @State var showLogo = false + var user: DBUser { ProfileVM.user } + + var body: some View { + if authVM.isLogin { + NavigationStack(path: $naviPath) { + List { + myCompanyView//我的公司 + myJobsView//我发布的工作 + }.listStyle().naviBarStyle().refreshable { await getUsersData() } + .toolbar { + ToolbarItem(placement: .principal) { Text("我的").tc().bold() } + ToolbarItem(placement: .topBarTrailing) { + Button { + naviPath.append(Navi5.Tosetting) + } label: { + Text("设置") + }.tc2().size15() + } + } + .navigationDestination(isPresented: $showLogo, destination: { + KFImage(URL(string: user.cLogoURLStr)).resizable().scaledToFit() + .toolbarVisibility(.hidden, for: .tabBar) + }) + .navigationDestination(for: Navi5.self) { navi in + switch navi { + case .AddCompany: PostCompanyView(naviPath: $naviPath) + case .EditCompany: PostCompanyView(isEditing: true, naviPath: $naviPath) + case .PLJobsToPLJob: PLJobView(canEdit: true, showBoss: false, naviPath: $naviPath) + case .PLJobToEdit: PostPLJobIView(isEditing: true,naviPath: $naviPath) + case .Tosetting: SettginsView(naviPath: $naviPath) + } + } + .overlay { + if ProfileVM.isFcSome { + BprogressView() + } + } + .onAppear { + Task { + if !ProfileVM.viewDidLoad { + await getUsersData() + ProfileVM.viewDidLoad = true + } else { await refreshIFNeed() } + } + } + } + } else { + SignInPhoneView() + } + + } + + func getUsersData() async { + do { try await ProfileVM.getUsersData() } catch { + hud.show("数据加载失败,请刷新\n错误信息:\n\(error.localizedDescription)") + print("个人页有数据加载失败:\(error)") + } + } + + func refreshIFNeed() async { + do { try await ProfileVM.refreshIFNeed() } catch { + hud.show("数据加载失败,请刷新\n错误信息:\n\(error.localizedDescription)") + print("个人页有数据加载失败:\(error)") + } + } + +} diff --git a/IOS_study/Profile/ProfileViewModel+.swift b/IOS_study/Profile/ProfileViewModel+.swift new file mode 100644 index 0000000..1f58fee --- /dev/null +++ b/IOS_study/Profile/ProfileViewModel+.swift @@ -0,0 +1,91 @@ +// +// ProfileViewModel+.swift +// IOS_study +// +// Created by CC-star on 2025/7/23. +// + +import Foundation + +extension ProfileViewModel { + @MainActor func getUser() async throws { + if isFcUser { return } + isFcUser = true + defer { isFcUser = false } + + user = try await UserManager.shared.findUserByREST() + } + + @MainActor func getPLJobs(isRefresh: Bool = false) async { + if isFcPLJobs { return } + isFcPLJobs = true + defer { isFcPLJobs = false } + if isRefresh { + currentPLPage = 0 + isPLFinshed = false + } + do { + if user.hasPLJobs { + let skip = kJobLimitS * currentPLPage + let newJobs = try await PLJobManager.shared.findJobsByREST(by: userID, limit: kJobLimitS, skip: skip,) + +// print(newJobs[0].title) +// print(newJobs[1].title) + if isRefresh { plJobs = newJobs } else { + plJobs.append(contentsOf: newJobs)//将一个数组添加到另外一个数组 + } + + if plJobs.isEmpty {//校准hasPLJobs + user.hasPLJobs = false + try await UserManager.shared.setTo(filed: DBUser.CodingKeys.hasPLJobs.rawValue, false) + } + + currentPLPage += 1//分页 + if newJobs.count < kJobLimitS || newJobs.isEmpty { + isPLFinshed = true + } + } else { + plJobs.removeAll() + } + } catch { + print("获取我发布的plJobs失败:\(error)") + } + } + @MainActor func getPLJobsCount() async { + if user.hasPLJobs { + async let aCount = PLJobManager.shared.findJobsCount(by: userID, onlyActive: true) + async let Count = PLJobManager.shared.findJobsCount(by: userID) + + (aPLJobsCount, pLJobsCount) = await (aCount, Count) + } + } + @MainActor func logOut() { + AuthManager.shared.logOut() + DispatchQueue.main.asyncAfter(deadline: .now() + 1) {//暂时用此方法,因为立刻reset的话旧画面仍旧会渲染,导致错误 + self.reset() + } + } + @MainActor func deletUserAll() async { + let userID = AuthManager.shared.getUserID() + await PLJobManager.shared.removeJobs(by: userID) + await UserManager.shared.removeUser(id: userID) + + if let lcuser = AuthManager.shared.getUser() { + try? lcuser.set("isNewUser", value: true) + try? await lcuser.save()//保存信息 + } + AuthManager.shared.logOut() + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.reset() } + } + + @MainActor func reset() { + viewDidLoad = false + user = DBUser() + plJobs.removeAll() + currentPLPage = 0 + isPLFinshed = false + aPLJobsCount = 0 + pLJobsCount = 0 + } + +} diff --git a/IOS_study/Profile/ProfileViewModel.swift b/IOS_study/Profile/ProfileViewModel.swift new file mode 100644 index 0000000..3cc8077 --- /dev/null +++ b/IOS_study/Profile/ProfileViewModel.swift @@ -0,0 +1,159 @@ +// +// ProfileViewModel.swift +// IOS_study +// +// Created by CC-star on 2025/7/15. +// + +import PhotosUI +import SwiftUI + +@Observable final class ProfileViewModel { + var viewDidLoad = false + //user + var user = DBUser() + var userID: String { user.id } + var draftUser = DBUser() + var isFcUser = false + var isCompanyImagesLoaded = false + var isPostingCompany = false + //发布的工作 + var plJobs: [PLJob] = [] + var aPLJobsCount = 0 + var pLJobsCount = 0 + var isFcPLJobs = false + var shouldRefreshMyPLJobs = false + //分页用 + var currentPLPage = 0 + var isPLFinshed = false + + //总的 + var isFcSome: Bool { isFcUser || isFcPLJobs } + + + @MainActor func getUsersData() async throws { + try await getUser() + async let getPLJobs: () = getPLJobs(isRefresh: true) + async let getPLJobsCount: () = getPLJobsCount() + + + let _ = await (getPLJobs, getPLJobsCount) + } + + @MainActor func refreshIFNeed() async throws { + if shouldRefreshMyPLJobs { + async let getPLJobs: () = getPLJobs(isRefresh: true) + async let getPLJobsCount: () = getPLJobsCount() + let _ = await (getPLJobs, getPLJobsCount) + shouldRefreshMyPLJobs = false + } + } + + var logoUIImage: UIImage?//可选类型,? -> 默认nil + var logoImageErrorMsg = "" + var logoItem: PhotosPickerItem? { + didSet { + //guard let logoItem -> guard let logoItem = logoItem + guard let logoItem else { return } + Task { + //把用户选择的item转换成。。。 + if let data = try? await logoItem.loadTransferable(type: Data.self), let uiImage = UIImage(data: data) { + logoImageErrorMsg = "" + logoUIImage = uiImage + } else { + print("PhotosPickerItem转换成图片失败") + logoImageErrorMsg = "上传图片失败,请重新上传" + } + self.logoItem = nil + } + } + } + var isLoadingCompanyImages = false + var companyUIImages: [UIImage] = [] + var companyUIImagesErrorMsg = "" + var companyItems: [PhotosPickerItem] = [] { + didSet { + if companyItems.isEmpty { return } + Task { @MainActor in + isLoadingCompanyImages = true + defer { + isLoadingCompanyImages = false + } + var newImages: [UIImage] = [] + for item in companyItems { + if let data = try? await item.loadTransferable(type: Data.self), let uiImage = UIImage(data: data) { + newImages.append(uiImage) + } + } + companyUIImages.append(contentsOf: newImages) + if newImages.count == companyItems.count { + companyUIImagesErrorMsg = "" + } else { + companyUIImagesErrorMsg = "有图片上传失败,请重新上传" + } + companyItems.removeAll() + } + } + } + @MainActor func loadCompanyLogoAndIamges() async { + isCompanyImagesLoaded = false + if !user.cLogoURLStr.isEmpty { + if let url = URL(string: user.cLogoURLStr), + let (data, _) = try? await URLSession.shared.data(from: url), + let uiImage = UIImage(data: data) { + logoUIImage = uiImage + } + } + if !user.cImageURLStrs.isEmpty { + var newImages: [UIImage] = [] + for cImageURLStr in user.cImageURLStrs { + if let url = URL(string: cImageURLStr), + let (data, _) = try? await URLSession.shared.data(from: url), + let uiImage = UIImage(data: data) { + newImages.append(uiImage) + } + } + companyUIImages = newImages + } + isCompanyImagesLoaded = true + } + + @MainActor func postCompany() async throws { + user = draftUser + //处理空格(必填部分) + user.cName = user.cName.trimmed + user.cCity = user.cCity.trimmed + //处理空格(选填部分) + if user.cAboutUS.isBlank { + user.cAboutUS = "" + } else { + user.cAboutUS = user.cAboutUS.trimmed + } + //并行上传图片,两种方法:async let 和 withThrowingTaskGroup + async let logoURLStr: String? = {//闭包 + guard let logoUIImage else { return nil } + return try await StorageManager.shared.saveImage(logoUIImage) + }() + if !companyUIImages.isEmpty { + let companyImageResults = try await withThrowingTaskGroup(of:((Int, String).self)) { group in + for (index, uiImage) in companyUIImages.enumerated() { + group.addTask { + let urlStr = try await StorageManager.shared.saveImage(uiImage) + return (index, urlStr) + } + } + var results: [(Int, String)] = [] + for try await result in group { + results.append(result) + } + return results + } + user.cImageURLStrs = companyImageResults.sorted(by: { $0.0 < $1.0 }).map { $0.1 } + } + if let clogoURLStr = try await logoURLStr { + user.cLogoURLStr = clogoURLStr + } + try await UserManager.shared.update(user: user) + } + +} diff --git a/IOS_study/Profile/SettginsView.swift b/IOS_study/Profile/SettginsView.swift new file mode 100644 index 0000000..b03f18a --- /dev/null +++ b/IOS_study/Profile/SettginsView.swift @@ -0,0 +1,84 @@ +// +// SettginsView.swift +// IOS_study +// +// Created by CC-star on 2025/7/10. +// + +import SwiftUI + +struct SettginsView: View { + @Environment(ProfileViewModel.self) var ProfileVM + @Environment(HUD.self) var hud + @Binding var naviPath: NavigationPath + @State var showSignOutDialog = false + @State var showRemoveUserDialog = false + @State var showRemoveUserAlert = false + var isLogin = true + var body: some View { + List { + Section("客服/商业合作") { + Button { + UIPasteboard.general.string = KdevContact + hud.show("复制成功") + } label: { + HStack { + Text("微信号") + Spacer() + Text(KdevContact).secondary() + } + }.settingsStyle() + } + + if isLogin { + Section("账号") { + HStack { + Button { + showSignOutDialog = true + } label: { + Text("退出登录").push(to: .leading).settingsStyle() + }.settingsStyle() + .confirmationDialog("确定退出吗?", isPresented: $showSignOutDialog, titleVisibility: .visible) { + Button("确定", role: .destructive) { + if !naviPath.isEmpty { naviPath.removeLast() } + ProfileVM.logOut() + NotificationCenter.default.post(name: .logoutSuccess, object: nil) + } + Button("取消", role: .cancel) { } + } + } + HStack { + Button { + showRemoveUserDialog = true + } label: { + Text("注销账号").push(to: .leading).settingsStyle() + }.settingsStyle() + .confirmationDialog("确定注销吗?", isPresented: $showRemoveUserDialog, titleVisibility: .visible) { + Button("确定", role: .destructive) { showRemoveUserAlert = true } + Button("取消", role: .cancel) { } + } + .alert("注销之后不可恢复,请再次确认", isPresented: $showRemoveUserAlert) { + Button("确定", role: .destructive) { Task { await ProfileVM.deletUserAll() } } + Button("取消", role: .cancel) { } + } + } + } + } + + }.listStyle(.grouped) + .toolbarBackgroundVisibility(.visible, for: .navigationBar) + .navigationBarBackButtonHidden(isLogin).navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { Text("设置") } + if isLogin { + ToolbarItem(placement: .topBarLeading) { + Button { + if !naviPath.isEmpty { naviPath.removeLast() } + } label: { + Image(systemName: "chevron.left").secondary().padding(.horizontal,6) + } + } + } + } + } +} diff --git a/IOS_study/Review/ReviewViewModel.swift b/IOS_study/Review/ReviewViewModel.swift new file mode 100644 index 0000000..109c051 --- /dev/null +++ b/IOS_study/Review/ReviewViewModel.swift @@ -0,0 +1,12 @@ +// +// ReviewViewModel.swift +// IOS_study +// +// Created by CC-star on 2025/7/25. +// + +import Foundation + +@Observable final class ReviewViewModel { + var drafReview = Review() +} diff --git a/IOS_study/TaBarView.swift b/IOS_study/TaBarView.swift new file mode 100644 index 0000000..848af61 --- /dev/null +++ b/IOS_study/TaBarView.swift @@ -0,0 +1,37 @@ +// +// TaBarView.swift +// IOS_study +// +// Created by CC-star on 2025/6/29. +// + +import SwiftUI + +struct TaBarView: View { + @Environment(TabBarViewModel.self) var vm + @State var authVM = AuthViewModel() + @State var plJobVM1 = PLJobViewModel() + @State var plJobVM4 = PLJobViewModel() + @State var plJobVM5 = PLJobViewModel() + var body: some View { + @Bindable var vm = vm + TabView(selection: $vm.selectedTab) { + Tab("工作", systemImage: "yensign.circle", value: "job") { + PLJobsView().toolbarBackground(.visible, for: .tabBar) + .environment(plJobVM1) + } + Tab("雇佣", systemImage: "plus.circle", value: "postJob") { + PostJobView().toolbarBackground(.visible, for: .tabBar) + .environment(plJobVM4) + } + Tab("我的", systemImage: "person.circle", value: "profile") { + ProfileView().toolbarBackground(.visible, for: .tabBar) + .environment(plJobVM5) + } + }.environment(authVM) + } +} + +#Preview { + TaBarView() +} diff --git a/IOS_study/TaBarViewModel.swift b/IOS_study/TaBarViewModel.swift new file mode 100644 index 0000000..2e8f427 --- /dev/null +++ b/IOS_study/TaBarViewModel.swift @@ -0,0 +1,20 @@ +// +// TaBarViewModel.swift +// IOS_study +// +// Created by CC-star on 2025/7/1. +// + +import Foundation + +@Observable class TabBarViewModel { + var selectedTab = "job" + var jobType = "兼职" + + var refreshJobs = false + @MainActor func goJobsAndRefresh(_ jobType: String) { + selectedTab = "job" + self.jobType = jobType + refreshJobs = true + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..978ad43 --- /dev/null +++ b/LICENSE @@ -0,0 +1,232 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and other kinds of works. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS + +0. Definitions. + +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on the Program. + +To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. + +A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. +A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. + + IOS_Study_BossDemo + Copyright (C) 2025 cc + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: + + IOS_Study_BossDemo Copyright (C) 2025 cc + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . diff --git a/README.md b/README.md new file mode 100644 index 0000000..e5acbb6 --- /dev/null +++ b/README.md @@ -0,0 +1,231 @@ +
+

IOS_Boss

+ +

+ Typing SVG +

+ +
+ +![iOS](https://img.shields.io/badge/iOS-18.0+-000000?style=for-the-badge&logo=ios&logoColor=white&labelColor=000000) +![Swift](https://img.shields.io/badge/Swift-5.0-FA7343?style=for-the-badge&logo=swift&logoColor=white&labelColor=FA7343) +![SwiftUI](https://img.shields.io/badge/SwiftUI-3.0+-1575F9?style=for-the-badge&logo=swift&logoColor=white&labelColor=1575F9) +![Xcode](https://img.shields.io/badge/Xcode-16.4-147EFB?style=for-the-badge&logo=xcode&logoColor=white&labelColor=147EFB) + + +
+ +
+

🚀 一款基于 SwiftUI 构建的简单 iOS 招聘应用

+

摸鱼ing . . .

+
+ +
+ +--- + +## 📖 项目简介 + +
+ + + +
+ + **iOS Boss** 是一个简单招聘应用演示项目,采用 **SwiftUI** 原生框架开发,集成现代化的 UI 设计理念和流畅的用户交互体验。 + + +
+
+ + +## 🛠️ 技术栈 + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
技术分类版本说明
🎨 前端框架SwiftUI 3.0+ | iOS 18.0+原生UI框架,声明式开发
☁️ 后端服务LeanCloud v17.11.0+数据存储 & 用户认证服务
🖼️ 图片处理Kingfisher v8.5.0+异步图片加载与智能缓存
🔄 下拉刷新MJRefresh v3.7.9+数据刷新组件
🎭 动画效果DotLottie Lottie 动画支持,提升交互体验
⚙️ 开发环境Swift 5.0 | Xcode 16.4开发语言与IDE环境
+
+ +## 📱 界面预览 + +
+

+🏠 核心浏览功能 +

+
+ + + + + + +
+
+

📋 工作卡片展示

+工作卡片样式 +

卡片设计,信息层次清晰

+
+
+
+

📖 职位详情页面

+工作详情样式 +

详细信息展示,完整职位描述

+
+
+ +
+

+📝 发布管理功能 +

+
+ + + + + + +
+
+

💼 发布招聘信息

+发布工作样式 +

信息发布界面,表单验证完善

+
+
+
+

🏢 发布公司信息

+发布公司 +

公司信息管理,支持图片上传

+
+
+ +
+

+🎭 交互体验功能 +

+
+ + + + + + +
+
+

💬 弹窗提醒系统

+弹窗提醒 +

简单提醒机制,提升用户体验

+
+
+
+

✅ 确认交互设计

+下线确定样式 +

安全的操作确认,防止误操作

+
+
+ +
+

+👤 个人中心功能 +

+
+ +
+
+

🎭 我的个人页面

+我的页面 +

用户中心

+
+
+ +--- + + +## 📚 学习资源 + +
+ + + + +
+

📖 官方文档

+ + +
+

🛠️ 开发工具

+ +
+
+ + +## 📋 备注 + +
+
+
    +
  • LocationHelper 定位功能待完善【未上传】
  • +
+
+
+ + +--- + +
+

+Light Bulb +如果这个项目对你有帮助,别忘了给个 ⭐ Star! +Star-Struck +

+ +

+感谢你的支持!🚀 +

+ +
\ No newline at end of file