init
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
BIN
assets/images/bk.jpg
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
assets/images/notify/dd.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
assets/images/notify/guanzhu.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
assets/images/notify/hudong.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
assets/images/notify/msr.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
assets/images/notify/qun.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
assets/images/notify/xitong.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
1
assets/images/svg/report.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1755509146760" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5991" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M906.66183476 910.82713117H117.33816524c-20.82648205 0-39.57031588-10.41324101-49.9835569-29.15707488s-10.41324101-39.57031588 0-57.27282562l394.66183476-683.10861105c10.41324101-17.70250974 29.15707487-29.15707487 49.9835569-29.15707487s39.57031588 10.41324101 49.9835569 29.15707487l394.66183476 683.10861105c10.41324101 17.70250974 10.41324101 39.57031588 0 57.27282562s-29.15707487 29.15707487-49.9835569 29.15707488zM512 144.41259189c-9.37191692 0-17.70250974 5.2066205-22.90913024 13.53721338l-394.66183475 683.10861102c-5.2066205 8.33059282-5.2066205 17.70250974 0 26.03310256s13.53721332 13.53721332 22.90913023 13.53721332h789.32366952c9.37191692 0 17.70250974-5.2066205 22.90913023-12.49588922 5.2066205-8.33059282 5.2066205-17.70250974 0-26.03310254l-394.66183475-683.10861104c-5.2066205-10.41324101-13.53721332-14.57853743-22.90913024-14.57853748z" p-id="5992" fill="#ffffff"></path><path d="M512 620.29770663c-8.33059282 0-15.61986151-7.2892687-15.61986151-15.61986152v-239.50454351c0-8.33059282 7.2892687-15.61986151 15.61986151-15.61986155s15.61986151 7.2892687 15.61986151 15.61986155v239.50454351c0 8.33059282-7.2892687 15.61986151-15.61986151 15.61986152zM512 768.16572913c-8.33059282 0-15.61986151-7.2892687-15.61986151-15.61986154v-62.47944612c0-8.33059282 7.2892687-15.61986151 15.61986151-15.61986152s15.61986151 7.2892687 15.61986151 15.61986152v62.47944612c0 8.33059282-7.2892687 15.61986151-15.61986151 15.61986154z" p-id="5993" fill="#ffffff"></path></svg>
|
After Width: | Height: | Size: 1.8 KiB |
@ -1,9 +1,11 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '12.0'
|
||||
|
||||
platform :ios, '12.0'
|
||||
# 允许拉取http资源
|
||||
# ENV['COCOAPODS_ALLOW_INSECURE_SOURCES'] = 'true'
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
|
@ -1,11 +1,25 @@
|
||||
PODS:
|
||||
- audioplayers_darwin (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- flutter_image_compress_common (1.0.0):
|
||||
- Flutter
|
||||
- Mantle
|
||||
- SDWebImage
|
||||
- SDWebImageWebPCoder
|
||||
- flutter_native_splash (2.4.3):
|
||||
- Flutter
|
||||
- flutter_upgrader (1.0.7):
|
||||
- Flutter
|
||||
- fluwx (0.0.1):
|
||||
- Flutter
|
||||
- fluwx/pay (= 0.0.1)
|
||||
- fluwx/pay (0.0.1):
|
||||
- Flutter
|
||||
- WechatOpenSDK-XCFramework (~> 2.0.4)
|
||||
- geolocator_apple (1.2.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@ -14,6 +28,21 @@ PODS:
|
||||
- Flutter
|
||||
- install_plugin (2.0.0):
|
||||
- Flutter
|
||||
- libwebp (1.3.2):
|
||||
- libwebp/demux (= 1.3.2)
|
||||
- libwebp/mux (= 1.3.2)
|
||||
- libwebp/sharpyuv (= 1.3.2)
|
||||
- libwebp/webp (= 1.3.2)
|
||||
- libwebp/demux (1.3.2):
|
||||
- libwebp/webp
|
||||
- libwebp/mux (1.3.2):
|
||||
- libwebp/demux
|
||||
- libwebp/sharpyuv (1.3.2)
|
||||
- libwebp/webp (1.3.2):
|
||||
- libwebp/sharpyuv
|
||||
- Mantle (2.2.0):
|
||||
- Mantle/extobjc (= 2.2.0)
|
||||
- Mantle/extobjc (2.2.0)
|
||||
- media_kit_libs_ios_video (1.0.4):
|
||||
- Flutter
|
||||
- media_kit_video (0.0.1):
|
||||
@ -31,26 +60,47 @@ PODS:
|
||||
- photo_manager (3.7.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- record_ios (1.0.0):
|
||||
- Flutter
|
||||
- SDWebImage (5.20.0):
|
||||
- SDWebImage/Core (= 5.20.0)
|
||||
- SDWebImage/Core (5.20.0)
|
||||
- SDWebImageWebPCoder (0.14.6):
|
||||
- libwebp (~> 1.0)
|
||||
- SDWebImage/Core (~> 5.17)
|
||||
- tencent_cloud_chat_push (8.6.7019):
|
||||
- Flutter
|
||||
- TIMPush (= 8.6.7019)
|
||||
- TXIMSDK_Plus_iOS_XCFramework
|
||||
- tencent_cloud_chat_sdk (8.0.0):
|
||||
- Flutter
|
||||
- HydraAsync
|
||||
- TXIMSDK_Plus_iOS_XCFramework (~> 8.6.7019)
|
||||
- TIMPush (8.6.7019):
|
||||
- TXIMSDK_Plus_iOS_XCFramework (>= 8.6.7019)
|
||||
- TXIMSDK_Plus_iOS_XCFramework (8.6.7019)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- video_thumbnail (0.0.1):
|
||||
- Flutter
|
||||
- libwebp
|
||||
- volume_controller (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
- WechatOpenSDK-XCFramework (2.0.4)
|
||||
|
||||
DEPENDENCIES:
|
||||
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_upgrader (from `.symlinks/plugins/flutter_upgrader/ios`)
|
||||
- fluwx (from `.symlinks/plugins/fluwx/ios`)
|
||||
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- install_plugin (from `.symlinks/plugins/install_plugin/ios`)
|
||||
@ -61,26 +111,41 @@ DEPENDENCIES:
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||
- record_ios (from `.symlinks/plugins/record_ios/ios`)
|
||||
- tencent_cloud_chat_push (from `.symlinks/plugins/tencent_cloud_chat_push/ios`)
|
||||
- tencent_cloud_chat_sdk (from `.symlinks/plugins/tencent_cloud_chat_sdk/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||
- video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- HydraAsync
|
||||
- libwebp
|
||||
- Mantle
|
||||
- SDWebImage
|
||||
- SDWebImageWebPCoder
|
||||
- TIMPush
|
||||
- TXIMSDK_Plus_iOS_XCFramework
|
||||
- WechatOpenSDK-XCFramework
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
audioplayers_darwin:
|
||||
:path: ".symlinks/plugins/audioplayers_darwin/darwin"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_image_compress_common:
|
||||
:path: ".symlinks/plugins/flutter_image_compress_common/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_upgrader:
|
||||
:path: ".symlinks/plugins/flutter_upgrader/ios"
|
||||
fluwx:
|
||||
:path: ".symlinks/plugins/fluwx/ios"
|
||||
geolocator_apple:
|
||||
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
||||
image_picker_ios:
|
||||
@ -101,26 +166,37 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
photo_manager:
|
||||
:path: ".symlinks/plugins/photo_manager/ios"
|
||||
record_ios:
|
||||
:path: ".symlinks/plugins/record_ios/ios"
|
||||
tencent_cloud_chat_push:
|
||||
:path: ".symlinks/plugins/tencent_cloud_chat_push/ios"
|
||||
tencent_cloud_chat_sdk:
|
||||
:path: ".symlinks/plugins/tencent_cloud_chat_sdk/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
video_player_avfoundation:
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
|
||||
video_thumbnail:
|
||||
:path: ".symlinks/plugins/video_thumbnail/ios"
|
||||
volume_controller:
|
||||
:path: ".symlinks/plugins/volume_controller/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
audioplayers_darwin: 4f9ca89d92d3d21cec7ec580e78ca888e5fb68bd
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
flutter_upgrader: 16a975eb987fc210cdf6bebffe0069a480f80523
|
||||
fluwx: 6bf9c5a3a99ad31b0de137dd92370a0d10a60f4b
|
||||
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
||||
HydraAsync: 8d589bd725b0224f899afafc9a396327405f8063
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
install_plugin: e17e38d6f504857748a3ec1299d8a2bbeeeea854
|
||||
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
|
||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
|
||||
@ -128,13 +204,20 @@ SPEC CHECKSUMS:
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
|
||||
record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b
|
||||
SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
|
||||
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
|
||||
tencent_cloud_chat_push: f87ae58098c2062b06e81f39fc53afc528395916
|
||||
tencent_cloud_chat_sdk: 0a406f1854a65aad2f853494c02a2e084a027ab2
|
||||
TIMPush: d0dfe96355ee413a7cacb2576f8aaa66f6073ab2
|
||||
TXIMSDK_Plus_iOS_XCFramework: cb54f7de6e30e1368c6831c6eff31c25393bbb98
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
|
||||
video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140
|
||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f
|
||||
|
||||
PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5
|
||||
PODFILE CHECKSUM: 866435f3a12ad92d8fb66fa46b52776da7e16ce5
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
@ -15,6 +15,8 @@
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
C8092B212E34A78000D25A0B /* WechatOpenSDK-XCFramework.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = C8092B202E34A78000D25A0B /* WechatOpenSDK-XCFramework.xcframework */; };
|
||||
C8092B222E34A78000D25A0B /* WechatOpenSDK-XCFramework.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C8092B202E34A78000D25A0B /* WechatOpenSDK-XCFramework.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
ECDFBB33253E89949730F7D8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 898AE91CA73F2F6E910D884D /* Pods_Runner.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@ -35,6 +37,7 @@
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
C8092B222E34A78000D25A0B /* WechatOpenSDK-XCFramework.xcframework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -63,6 +66,8 @@
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
A9774DDA95C7FD895F6925A4 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
BB84C2FA9C50ACAF0C376254 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
C8092B202E34A78000D25A0B /* WechatOpenSDK-XCFramework.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = "WechatOpenSDK-XCFramework.xcframework"; path = "Pods/WechatOpenSDK-XCFramework/WechatOpenSDK-XCFramework.xcframework"; sourceTree = "<group>"; };
|
||||
C891EF1D2E43F9730021EB39 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||
DCA23AF172275D04ECB63EFB /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
E3EC116A6CCDD06C6D4615E2 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@ -72,6 +77,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C8092B212E34A78000D25A0B /* WechatOpenSDK-XCFramework.xcframework in Frameworks */,
|
||||
ECDFBB33253E89949730F7D8 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -143,6 +149,7 @@
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C891EF1D2E43F9730021EB39 /* Runner.entitlements */,
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
@ -158,6 +165,7 @@
|
||||
9CE4C2341F34F9A85A1D3EED /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C8092B202E34A78000D25A0B /* WechatOpenSDK-XCFramework.xcframework */,
|
||||
898AE91CA73F2F6E910D884D /* Pods_Runner.framework */,
|
||||
1AE799326ED7557212A901E0 /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
@ -487,8 +495,11 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = VZ6V44Q3T4;
|
||||
DEVELOPMENT_TEAM = 9C9VWBX77X;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -496,8 +507,9 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 4.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.wzj41.test1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = cn.net.wzj.mall;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@ -671,8 +683,11 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = VZ6V44Q3T4;
|
||||
DEVELOPMENT_TEAM = 9C9VWBX77X;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -680,8 +695,9 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 4.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.wzj41.test1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = cn.net.wzj.mall;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@ -695,8 +711,11 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = VZ6V44Q3T4;
|
||||
DEVELOPMENT_TEAM = 9C9VWBX77X;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -704,8 +723,9 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 4.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.wzj41.test1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = cn.net.wzj.mall;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
|
@ -1,8 +1,13 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import Flutter
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
// Add these two import lines
|
||||
import TIMPush
|
||||
import tencent_cloud_chat_push
|
||||
|
||||
// Add `, TIMPushDelegate` to the following line
|
||||
@UIApplicationMain
|
||||
@objc class AppDelegate: FlutterAppDelegate, TIMPushDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
@ -10,4 +15,25 @@ import UIKit
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
// To be deprecated,please use the new field businessID below.
|
||||
@objc func offlinePushCertificateID() -> Int32 {
|
||||
return TencentCloudChatPushFlutterModal.shared.offlinePushCertificateID();
|
||||
}
|
||||
|
||||
// Add this function
|
||||
@objc func businessID() -> Int32 {
|
||||
return TencentCloudChatPushFlutterModal.shared.businessID();
|
||||
}
|
||||
|
||||
// Add this function
|
||||
@objc func applicationGroupID() -> String {
|
||||
return TencentCloudChatPushFlutterModal.shared.applicationGroupID()
|
||||
}
|
||||
|
||||
// Add this function
|
||||
@objc func onRemoteNotificationReceived(_ notice: String?) -> Bool {
|
||||
TencentCloudChatPushPlugin.shared.tryNotifyDartOnNotificationClickEvent(notice)
|
||||
return true
|
||||
}
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
@ -14,13 +16,14 @@
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-16" y="-40"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
@ -7,7 +7,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>无终见41</string>
|
||||
<string>无终街</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@ -22,20 +22,44 @@
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>weixin</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>wxebcdaea31881caab</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>weixin</string>
|
||||
<string>wechat</string>
|
||||
<string>weixinULAPI</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>App需要使用您的相机进行拍摄</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>App需要访问麦克风用于视频录制</string>
|
||||
<string>App需要访问麦克风用于发送语音消息</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>App需要权限以保存视频到您的相册</string>
|
||||
<string>App需要权限以保存图片或视频到您的相册</string>
|
||||
<key>NSPhotoLibraryLimitedUsageDescription</key>
|
||||
<string>App需要访问部分照片用于选择视频</string>
|
||||
<string>App需要访问部分照片用于选择图片或视频</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>App需要访问您的相册用于选择视频</string>
|
||||
<string>App需要访问您的相册用于选择图片或视频</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
@ -59,5 +83,15 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:wuzhongjie.com.cn</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
9
ios/Runner/Runner.entitlements
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<!-- <string>production</string> -->
|
||||
</dict>
|
||||
</plist>
|
@ -1,24 +1,118 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:loopin/IM/im_service.dart';
|
||||
import 'package:loopin/models/conversation_type.dart' as myConversationType;
|
||||
import 'package:loopin/models/conversation_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_filter.dart';
|
||||
|
||||
class ChatController extends GetxController {
|
||||
RxInt count = 100.obs; // 每页条数
|
||||
RxInt count = 20.obs; // 每页条数
|
||||
RxString nextSeq = '0'.obs; // 页码
|
||||
RxBool isFinished = false.obs; // 是否拉取完?默认未拉取完
|
||||
|
||||
final chatList = <ConversationViewModel>[].obs;
|
||||
|
||||
// 获取会话列表
|
||||
void initChatData() {
|
||||
chatList.value = <ConversationViewModel>[];
|
||||
nextSeq.value = '0';
|
||||
isFinished.value = false;
|
||||
}
|
||||
|
||||
// 获取所有会话列表
|
||||
void getConversationList() async {
|
||||
if (isFinished.value) {
|
||||
// 拉取完数据了,直接结束
|
||||
return;
|
||||
}
|
||||
final res = await ImService.instance.getConversationList(nextSeq.value, count.value);
|
||||
|
||||
if (!res.success || res.data == null) return;
|
||||
|
||||
final List<ConversationViewModel> convList = res.data;
|
||||
// for (var conv in convList) {
|
||||
// logger.i('基本会话: ${conv.conversation.toLogString()}, 头像: ${conv.faceUrl}');
|
||||
// }
|
||||
|
||||
chatList.value = convList;
|
||||
for (var conv in convList) {
|
||||
logger.i('基本会话: ${conv.conversation.toJson()}, 会话ID: ${conv.conversation.conversationID}');
|
||||
}
|
||||
|
||||
chatList.addAll(convList);
|
||||
// 不包含noFriend才执行加载数据逻辑,分页加载时候过滤
|
||||
final hasNoFriend = chatList.any((item) => item.conversation.conversationGroupList?.contains(myConversationType.ConversationType.noFriend.name) ?? false);
|
||||
if (!hasNoFriend) {
|
||||
getNoFriendData();
|
||||
}
|
||||
}
|
||||
|
||||
///构建陌生人消息菜单入口
|
||||
void getNoFriendData({V2TimConversation? csion}) async {
|
||||
// 检测会话列表是否已有陌生人消息菜单
|
||||
final hasNoFriend = chatList.any((item) => item.conversation.conversationGroupList?.contains(myConversationType.ConversationType.noFriend.name) ?? false);
|
||||
if (hasNoFriend) {
|
||||
// 已经有了入口
|
||||
final ConversationViewModel matchItem = chatList.firstWhere(
|
||||
(item) => item.conversation.conversationGroupList?.contains(myConversationType.ConversationType.noFriend.name) ?? false,
|
||||
);
|
||||
// 获取陌生人未读总数
|
||||
final unreadTotal = await ImService.instance.getUnreadMessageCountByFilter(
|
||||
filter: V2TimConversationFilter(
|
||||
conversationGroup: myConversationType.ConversationType.noFriend.name,
|
||||
hasUnreadCount: true,
|
||||
),
|
||||
);
|
||||
matchItem.conversation.lastMessage = csion!.lastMessage;
|
||||
matchItem.conversation.unreadCount = unreadTotal.data;
|
||||
chatList.refresh();
|
||||
return;
|
||||
}
|
||||
// 没有则执行创建逻辑
|
||||
final res = await ImService.instance.getConversationListByFilter(
|
||||
filter: V2TimConversationFilter(conversationGroup: myConversationType.ConversationType.noFriend.name),
|
||||
nextSeq: 0,
|
||||
count: 1,
|
||||
);
|
||||
if (res.success && res.data != null) {
|
||||
final convList = res.data!.conversationList ?? [];
|
||||
if (convList.isNotEmpty) {
|
||||
// logger.i(res.data!.toJson());
|
||||
// 有陌生人消息,1.获取未读数,2.组装converstaionviewmodel
|
||||
final unread = await ImService.instance.getUnreadMessageCountByFilter(
|
||||
filter: V2TimConversationFilter(
|
||||
conversationGroup: myConversationType.ConversationType.noFriend.name,
|
||||
hasUnreadCount: true,
|
||||
),
|
||||
);
|
||||
if (unread.success) {
|
||||
final conv = convList.first;
|
||||
final faceUrl = 'assets/images/notify/msr.png';
|
||||
conv.showName = '陌生人消息';
|
||||
conv.unreadCount = unread.data;
|
||||
final createItem = ConversationViewModel(
|
||||
conversation: conv,
|
||||
faceUrl: faceUrl,
|
||||
);
|
||||
final newList = List<ConversationViewModel>.from(chatList);
|
||||
newList.add(createItem);
|
||||
newList.sort((a, b) {
|
||||
final atime = a.conversation.lastMessage?.timestamp ?? 0;
|
||||
final btime = b.conversation.lastMessage?.timestamp ?? 0;
|
||||
return btime.compareTo(atime); // 降序
|
||||
});
|
||||
chatList.value = newList;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 按会话分组查询 getConversationListByFilter
|
||||
// void getConversationList() async {
|
||||
// final res = await ImService.instance.getConversationListByFilter(
|
||||
// filter: V2TimConversationFilter(conversationGroup: null),
|
||||
// nextSeq: nextSeq.value,
|
||||
// );
|
||||
// final convList = res.data!.conversationList;
|
||||
// logger.i(res.data!.toJson());
|
||||
// chatList.value = convList;
|
||||
// // for (var element in convList ?? []) {
|
||||
// // logger.i(element.toJson());
|
||||
// // // 你可以在这里继续处理 element
|
||||
// // }
|
||||
// }
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:loopin/IM/im_message.dart';
|
||||
import 'package:loopin/IM/im_service.dart';
|
||||
@ -8,8 +9,10 @@ class ChatDetailController extends GetxController {
|
||||
final String userID;
|
||||
|
||||
ChatDetailController({required this.userID});
|
||||
final ScrollController chatController = ScrollController();
|
||||
|
||||
final RxList<V2TimMessage> chatList = <V2TimMessage>[].obs;
|
||||
final RxBool isFriend = true.obs;
|
||||
|
||||
void updateChatListWithTimeLabels(List<V2TimMessage> originMessages) async {
|
||||
final idRes = await ImService.instance.selfUserId();
|
||||
@ -37,17 +40,54 @@ class ChatDetailController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
// 把当前消息先插入后插入标签
|
||||
displayMessages.add(current);
|
||||
// if (i == 0) {
|
||||
// // 第一条一定插时间
|
||||
// needInsertLabel = true;
|
||||
// } else {
|
||||
// final prev = originMessages[i - 1];
|
||||
// final prevTimestamp = prev.timestamp ?? 0;
|
||||
// final diff = currentTimestamp - prevTimestamp;
|
||||
|
||||
// if (diff > 180) {
|
||||
// needInsertLabel = true;
|
||||
// }
|
||||
// }
|
||||
// 把当前消息先插入,label后插入
|
||||
displayMessages.add(current);
|
||||
if (needInsertLabel) {
|
||||
final labelTime = Utils().formatChatTime(currentTimestamp);
|
||||
final timeLabel = await IMMessage().insertTimeLabel(labelTime, selfUserId);
|
||||
displayMessages.add(timeLabel.data);
|
||||
}
|
||||
}
|
||||
|
||||
// 新加载的记录放在最上面
|
||||
chatList.addAll(displayMessages);
|
||||
}
|
||||
}
|
||||
|
||||
///滚动
|
||||
void scrollToBottom() {
|
||||
if (chatController.hasClients) {
|
||||
chatController.animateTo(
|
||||
0,
|
||||
duration: Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
// Future.delayed(Duration(milliseconds: 300), () {
|
||||
// if (chatController.hasClients) {
|
||||
// chatController.animateTo(
|
||||
// chatController.position.maxScrollExtent,
|
||||
// duration: Duration(milliseconds: 200),
|
||||
// curve: Curves.easeOut,
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
chatController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
137
lib/IM/controller/im_user_info_controller.dart
Normal file
@ -0,0 +1,137 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:loopin/IM/im_service.dart';
|
||||
import 'package:shirne_dialog/shirne_dialog.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart';
|
||||
|
||||
class ImUserInfoController extends GetxController {
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
refreshUserInfo();
|
||||
logger.i('IM用户信息初始化');
|
||||
}
|
||||
|
||||
V2TimUserFullInfo? rawUserInfo;
|
||||
|
||||
final userID = ''.obs;
|
||||
final nickname = ''.obs;
|
||||
final faceUrl = ''.obs;
|
||||
final signature = ''.obs;
|
||||
final gender = 0.obs;
|
||||
final allowType = 0.obs;
|
||||
final customInfo = <String, String>{
|
||||
"coverBg": "",
|
||||
"area": "",
|
||||
"areaCode": "",
|
||||
"openId": "",
|
||||
}.obs;
|
||||
final role = 0.obs;
|
||||
final level = 0.obs;
|
||||
final birthday = 0.obs;
|
||||
|
||||
void init(V2TimUserFullInfo userInfo) {
|
||||
logger.i(userInfo.toJson());
|
||||
rawUserInfo = userInfo;
|
||||
userID.value = userInfo.userID ?? '';
|
||||
nickname.value = userInfo.nickName ?? '';
|
||||
faceUrl.value = userInfo.faceUrl ?? '';
|
||||
signature.value = userInfo.selfSignature ?? '';
|
||||
gender.value = userInfo.gender ?? 0;
|
||||
allowType.value = userInfo.allowType ?? 0;
|
||||
customInfo.assignAll(userInfo.customInfo ??
|
||||
{
|
||||
"coverBg": "",
|
||||
"area": "",
|
||||
"areaCode": "",
|
||||
"openId": "",
|
||||
});
|
||||
|
||||
role.value = userInfo.role ?? 0;
|
||||
level.value = userInfo.level ?? 0;
|
||||
birthday.value = userInfo.birthday ?? 0;
|
||||
}
|
||||
|
||||
void refreshUserInfo() async {
|
||||
try {
|
||||
final updatedUserInfo = await ImService.instance.selfInfo();
|
||||
if (updatedUserInfo.success) {
|
||||
init(updatedUserInfo.data);
|
||||
}
|
||||
} catch (e) {
|
||||
print('刷新用户信息失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新昵称
|
||||
Future<bool> updateNickname(newnickname) async {
|
||||
final res = await ImService.instance.setSelfInfo(userFullInfo: V2TimUserFullInfo(nickName: newnickname));
|
||||
if (res.success) {
|
||||
nickname.value = newnickname;
|
||||
} else {
|
||||
logger.i(res.desc);
|
||||
if (res.code == 80001) {
|
||||
MyDialog.toast('昵称违规', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
||||
}
|
||||
}
|
||||
return res.success;
|
||||
}
|
||||
|
||||
/// 更新简介
|
||||
Future<bool> updateSignature(newsignature) async {
|
||||
final res = await ImService.instance.setSelfInfo(userFullInfo: V2TimUserFullInfo(selfSignature: newsignature));
|
||||
if (res.success) {
|
||||
signature.value = newsignature;
|
||||
} else {
|
||||
logger.i(res.desc);
|
||||
if (res.code == 80001) {
|
||||
MyDialog.toast('简介内容违规', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
||||
}
|
||||
}
|
||||
return res.success;
|
||||
}
|
||||
|
||||
/// 更新头像
|
||||
Future<void> updateFaceUrl() async {
|
||||
if (faceUrl.value.trim().isEmpty) return;
|
||||
await ImService.instance.setSelfInfo(userFullInfo: V2TimUserFullInfo(faceUrl: faceUrl.value));
|
||||
}
|
||||
|
||||
/// 更新背景图
|
||||
Future<void> updateCover() async {
|
||||
final coverBg = customInfo['coverBg'];
|
||||
if (coverBg == null || coverBg.trim().isEmpty) return;
|
||||
await ImService.instance.setSelfInfo(userFullInfo: V2TimUserFullInfo(customInfo: customInfo));
|
||||
}
|
||||
|
||||
/// 更新openId
|
||||
Future<void> updateOpenId() async {
|
||||
final openId = customInfo['openId'];
|
||||
if (openId == null || openId.trim().isEmpty) return;
|
||||
await ImService.instance.setSelfInfo(userFullInfo: V2TimUserFullInfo(customInfo: customInfo));
|
||||
}
|
||||
// customInfo.update("coverBg", (value) => coverBgUrl);
|
||||
|
||||
/// 更新所在地
|
||||
Future<void> updateArea() async {
|
||||
final area = customInfo['area'];
|
||||
if (area == null || area.trim().isEmpty) return;
|
||||
final areaCode = customInfo['areaCode'];
|
||||
if (areaCode == null || areaCode.trim().isEmpty) return;
|
||||
await ImService.instance.setSelfInfo(userFullInfo: V2TimUserFullInfo(customInfo: customInfo));
|
||||
}
|
||||
|
||||
///更新生日
|
||||
Future<void> updateBirthday() async {
|
||||
if (birthday.value < 0) return;
|
||||
await ImService.instance.setSelfInfo(userFullInfo: V2TimUserFullInfo(birthday: birthday.value));
|
||||
}
|
||||
|
||||
///更新性别
|
||||
Future<void> updateGender() async {
|
||||
if (gender.value < 0) return;
|
||||
await ImService.instance.setSelfInfo(userFullInfo: V2TimUserFullInfo(gender: gender.value));
|
||||
}
|
||||
|
||||
/// updateAvatar、updateSignature 等方法
|
||||
}
|
@ -1,9 +1,12 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:loopin/IM/controller/chat_controller.dart';
|
||||
import 'package:loopin/IM/controller/tab_bar_controller.dart';
|
||||
import 'package:loopin/IM/im_core.dart';
|
||||
import 'package:loopin/IM/im_service.dart';
|
||||
import 'package:loopin/models/conversation_type.dart';
|
||||
import 'package:loopin/models/tab_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/V2TimConversationListener.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_filter.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
|
||||
|
||||
class GlobalBadge extends GetxController {
|
||||
@ -21,14 +24,103 @@ class GlobalBadge extends GetxController {
|
||||
logger.i('未读数发生变化$count');
|
||||
totalUnread.value = count;
|
||||
Get.find<TabBarController>().setBadge(TabType.chat, totalUnread.value);
|
||||
// 更新会话列表
|
||||
Get.find<ChatController>().getConversationList();
|
||||
},
|
||||
onNewConversation: (List<V2TimConversation> conversationList) {
|
||||
for (var conv in conversationList) {
|
||||
logger.i("新会话创建:${conv.conversationGroupList}");
|
||||
handleCoverstion(conv);
|
||||
}
|
||||
},
|
||||
onConversationChanged: (List<V2TimConversation> conversationList) async {
|
||||
logger.w('会话变更:会话分组:${conversationList.first.conversationGroupList},会话内容${conversationList.first.toLogString()}');
|
||||
final ctl = Get.find<ChatController>();
|
||||
final updatedIds = conversationList.map((e) => e.conversationID).toSet();
|
||||
logger.w('要变更的会话id:$updatedIds');
|
||||
for (int i = 0; i < ctl.chatList.length; i++) {
|
||||
final chatItem = ctl.chatList[i];
|
||||
logger.w('需要更新的ID:${chatItem.conversation.conversationID}');
|
||||
if (updatedIds.contains(chatItem.conversation.conversationID)) {
|
||||
final updatedConv = conversationList.firstWhere(
|
||||
(c) => c.conversationID == chatItem.conversation.conversationID,
|
||||
orElse: () => V2TimConversation(conversationID: ''),
|
||||
);
|
||||
|
||||
if (updatedConv.conversationID != '' && (updatedConv.conversationGroupList?.contains(ConversationType.noFriend.name) ?? false)) {
|
||||
// 单独处理陌生人会话
|
||||
final unread = await ImService.instance.getUnreadMessageCountByFilter(
|
||||
filter: V2TimConversationFilter(
|
||||
conversationGroup: ConversationType.noFriend.name,
|
||||
hasUnreadCount: true,
|
||||
),
|
||||
);
|
||||
chatItem.conversation.lastMessage = updatedConv.lastMessage;
|
||||
chatItem.conversation.unreadCount = unread.data; // 获取陌生人未读总数
|
||||
} else {
|
||||
// 其他类型统一更新处理
|
||||
chatItem.conversation = updatedConv;
|
||||
}
|
||||
}
|
||||
}
|
||||
//重新排序
|
||||
ctl.chatList.sort((a, b) {
|
||||
final atime = a.conversation.lastMessage?.timestamp ?? 0;
|
||||
final btime = b.conversation.lastMessage?.timestamp ?? 0;
|
||||
return btime.compareTo(atime); // 降序
|
||||
});
|
||||
ctl.chatList.refresh();
|
||||
},
|
||||
);
|
||||
final ctl = Get.find<ChatController>();
|
||||
ctl.getConversationList();
|
||||
_initUnreadCount();
|
||||
_addListener();
|
||||
}
|
||||
|
||||
// final rr = await ImService.instance.deleteConversationsFromGroup(
|
||||
// conversationIDList: [cov.conversationID],
|
||||
// groupName: 'noFriend',
|
||||
// );
|
||||
// logger.w(rr.desc);
|
||||
|
||||
/// 新建会话时候,根据消息的自定义属性给会话分组
|
||||
void handleCoverstion(V2TimConversation cov) async {
|
||||
final message = cov.lastMessage;
|
||||
final isSelfSend = message!.isSelf; // 是否本人发送的消息
|
||||
final typeEnum = conversationTypeFromString(message.cloudCustomData); // 会话类型
|
||||
final needAdd = cov.conversationGroupList!.isEmpty == true; // 当前会话是否已加入了分组中
|
||||
if (typeEnum != null && needAdd && isSelfSend == false) {
|
||||
logger.i('当前会话的类型要加入的组是:$typeEnum');
|
||||
// 当前会话需要进行分组,检测 组 是否存在
|
||||
final hasGroupRes = await ImService.instance.getConversationGroupList();
|
||||
if (hasGroupRes.success) {
|
||||
final exists = hasGroupRes.data?.any((item) => item == typeEnum) ?? false;
|
||||
if (!exists) {
|
||||
// 组不存在,创建组并把会话加入group中
|
||||
await ImService.instance.createConversationGroup(
|
||||
groupName: typeEnum,
|
||||
conversationIDList: ['c2c_${message.sender}'],
|
||||
);
|
||||
logger.i('首次创建会话分组$typeEnum');
|
||||
} else {
|
||||
// 分组存在直接添加
|
||||
await ImService.instance.addConversationsToGroup(
|
||||
groupName: typeEnum,
|
||||
conversationIDList: ['c2c_${message.sender}'],
|
||||
);
|
||||
logger.i('添加会话分组$typeEnum成功');
|
||||
}
|
||||
if (typeEnum == ConversationType.noFriend.name) {
|
||||
//陌生人分组特殊处理 满足分组条件且已经有分组,
|
||||
final ctl = Get.find<ChatController>();
|
||||
// 这个方法执行的逻辑:已有则刷新菜单入口数据,没有则创建菜单入口
|
||||
ctl.getNoFriendData(csion: cov);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.w('不需要进行分组');
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化时获取一次未读总数
|
||||
void _initUnreadCount() async {
|
||||
final res = await TencentImSDKPlugin.v2TIMManager.getConversationManager().getTotalUnreadMessageCount();
|
||||
|
@ -14,14 +14,16 @@ class ImCore {
|
||||
|
||||
final res = await TencentImSDKPlugin.v2TIMManager.initSDK(
|
||||
sdkAppID: sdkAppId,
|
||||
loglevel: LogLevelEnum.V2TIM_LOG_ALL,
|
||||
loglevel: LogLevelEnum.V2TIM_LOG_ERROR,
|
||||
listener: V2TimSDKListener(
|
||||
onConnectSuccess: () => logger.i("IM连接成功"),
|
||||
onConnectSuccess: () {
|
||||
logger.i("IM连接成功");
|
||||
},
|
||||
onConnectFailed: (code, error) => logger.e("IM连接失败: $code $error"),
|
||||
onKickedOffline: () => logger.w("IM被踢下线"),
|
||||
onUserSigExpired: () => logger.w("UserSig 过期"),
|
||||
onSelfInfoUpdated: (V2TimUserFullInfo info) {
|
||||
logger.i("用户信息更新: ${info.nickName}");
|
||||
logger.i("用户信息更新: ${info.toJson()}");
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -29,6 +31,7 @@ class ImCore {
|
||||
if (res.code == 0) {
|
||||
_isInitialized = true;
|
||||
logger.i("IM SDK 初始化成功");
|
||||
|
||||
return true;
|
||||
} else {
|
||||
logger.e("IM SDK 初始化失败: ${res.code} - ${res.desc}");
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:loopin/utils/notification_banner.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/V2TimFriendshipListener.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_application.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_info.dart';
|
||||
@ -16,7 +17,6 @@ class ImFriendListeners {
|
||||
_listener = V2TimFriendshipListener(onFriendApplicationListAdded: (List<V2TimFriendApplication> list) async {
|
||||
//好友请求数量增加的回调
|
||||
//applicationList 新增的好友请求信息列表
|
||||
logger.i('收到好友申请: ${list.map((e) => e.userID).join(",")}');
|
||||
}, onFriendApplicationListRead: () async {
|
||||
//好友请求已读的回调
|
||||
}, onFriendApplicationListDeleted: (List<String> userIDList) async {
|
||||
@ -25,11 +25,15 @@ class ImFriendListeners {
|
||||
}, onFriendListAdded: (List<V2TimFriendInfo> users) async {
|
||||
//好友列表增加人员的回调
|
||||
//users 新增的好友信息列表
|
||||
logger.i('新增好友: ${users.map((u) => u.userID).join(",")}');
|
||||
for (var item in users) {
|
||||
logger.i('新增好友:${item.toLogString()}');
|
||||
}
|
||||
}, onFriendListDeleted: (List<String> userList) async {
|
||||
//好友列表减少人员的回调
|
||||
//userList 减少的好友id列表
|
||||
logger.i('删除好友: ${userList.join(",")}');
|
||||
for (var item in userList) {
|
||||
logger.i('新增好友:$item');
|
||||
}
|
||||
}, onFriendInfoChanged: (List<V2TimFriendInfo> list) async {
|
||||
//好友信息改变的回调
|
||||
//infoList 好友信息改变的好友列表
|
||||
@ -43,14 +47,27 @@ class ImFriendListeners {
|
||||
}, onMyFollowingListChanged: (List<V2TimUserFullInfo> userInfoList, bool isAdd) async {
|
||||
if (isAdd) {
|
||||
// 关注列表新增用户的通知
|
||||
for (var item in userInfoList) {
|
||||
logger.i('我新关注的人:${item.toJson()}');
|
||||
}
|
||||
} else {
|
||||
// 关注列表删除用户的通知
|
||||
for (var item in userInfoList) {
|
||||
logger.i('我取消关注了:${item.toJson()}');
|
||||
}
|
||||
}
|
||||
}, onMyFollowersListChanged: (List<V2TimUserFullInfo> userInfoList, bool isAdd) async {
|
||||
if (isAdd) {
|
||||
// 粉丝列表新增用户的通知
|
||||
for (var item in userInfoList) {
|
||||
logger.i('新增粉丝:${item.toJson()}');
|
||||
}
|
||||
NotificationBanner.foucs(userInfoList.last);
|
||||
} else {
|
||||
// 粉丝列表删除用户的通知
|
||||
for (var item in userInfoList) {
|
||||
logger.i('掉粉:${item.toJson()}');
|
||||
}
|
||||
}
|
||||
}, onMutualFollowersListChanged: (List<V2TimUserFullInfo> userInfoList, bool isAdd) async {
|
||||
if (isAdd) {
|
||||
|
@ -1,20 +1,27 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:loopin/IM/controller/im_user_info_controller.dart';
|
||||
import 'package:loopin/IM/im_result.dart';
|
||||
import 'package:loopin/utils/parse_message_summary.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_priority_enum.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/offlinePushInfo.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_msg_create_info_result.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
|
||||
|
||||
final logger = Logger();
|
||||
|
||||
class IMMessage {
|
||||
/// 发送文本消息
|
||||
Future<ImResult> sendText({
|
||||
required String text,
|
||||
final logger = Logger();
|
||||
|
||||
/// 1.发送消息
|
||||
Future<ImResult> sendMessage({
|
||||
required V2TimMessage msg,
|
||||
String? toUserID,
|
||||
String? groupID,
|
||||
String? data,
|
||||
String? cloudCustomData,
|
||||
}) async {
|
||||
// 必须且只能设置一个:toUserID(单聊)或 groupID(群聊)
|
||||
if ((toUserID == null && groupID == null) || (toUserID != null && groupID != null)) {
|
||||
@ -24,40 +31,44 @@ class IMMessage {
|
||||
desc: "只能指定一个 receiver(toUserID)或 groupID",
|
||||
);
|
||||
}
|
||||
|
||||
// 创建消息
|
||||
final createRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().createTextMessage(text: text);
|
||||
|
||||
if (createRes.code != 0 || createRes.data == null) {
|
||||
return ImResult(
|
||||
success: false,
|
||||
code: createRes.code,
|
||||
desc: "创建消息失败",
|
||||
);
|
||||
if (cloudCustomData != null) {
|
||||
msg.cloudCustomData = cloudCustomData;
|
||||
}
|
||||
final V2TimMessage? messageInfo = createRes.data?.messageInfo;
|
||||
|
||||
// 解析消息类型
|
||||
V2TimValueCallback<V2TimMessage> sendRes;
|
||||
|
||||
// final controller = Get.find<ChatDetailController>();
|
||||
final myInfo = Get.find<ImUserInfoController>();
|
||||
logger.w('启用默认title:${myInfo.nickname.value}');
|
||||
// 单聊
|
||||
if (toUserID != null) {
|
||||
sendRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage(
|
||||
message: messageInfo,
|
||||
message: msg,
|
||||
receiver: toUserID,
|
||||
// onSyncMsgID: (msgID) async {
|
||||
// 这里立刻拿到消息ID,可以提前把这条消息展示到列表中(发送中状态)有时间再改吧
|
||||
// 根据类型,创建对应的elem;
|
||||
// logger.w(msg.imageElem!.toLogString());
|
||||
// controller.chatList.add(msg.imageElem);
|
||||
// controller.scrollToBottom();
|
||||
// },
|
||||
groupID: "",
|
||||
priority: MessagePriorityEnum.V2TIM_PRIORITY_DEFAULT,
|
||||
onlineUserOnly: false,
|
||||
isExcludedFromUnreadCount: false,
|
||||
isExcludedFromLastMessage: false,
|
||||
needReadReceipt: false,
|
||||
offlinePushInfo: OfflinePushInfo(title: "新消息", desc: text),
|
||||
cloudCustomData: "",
|
||||
offlinePushInfo: OfflinePushInfo(
|
||||
title: myInfo.nickname.value,
|
||||
desc: parseMessageSummary(msg),
|
||||
ext: jsonEncode({"userID": myInfo.userID.value, "title": myInfo.nickname.value}),
|
||||
),
|
||||
cloudCustomData: cloudCustomData,
|
||||
localCustomData: "",
|
||||
);
|
||||
} else {
|
||||
// 群聊
|
||||
sendRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage(
|
||||
message: messageInfo,
|
||||
message: msg,
|
||||
receiver: "",
|
||||
groupID: groupID!,
|
||||
priority: MessagePriorityEnum.V2TIM_PRIORITY_DEFAULT,
|
||||
@ -65,7 +76,7 @@ class IMMessage {
|
||||
isExcludedFromUnreadCount: false,
|
||||
isExcludedFromLastMessage: false,
|
||||
needReadReceipt: false,
|
||||
offlinePushInfo: OfflinePushInfo(title: "新群聊消息", desc: text),
|
||||
offlinePushInfo: OfflinePushInfo(title: '群聊消息', desc: parseMessageSummary(msg)),
|
||||
cloudCustomData: "",
|
||||
localCustomData: "",
|
||||
);
|
||||
@ -79,62 +90,18 @@ class IMMessage {
|
||||
);
|
||||
}
|
||||
|
||||
/// 发送自定义消息
|
||||
Future<ImResult> sendCustomMessage({
|
||||
/// 2=创建自定义消息
|
||||
Future<ImResult<V2TimMsgCreateInfoResult>> createCustomMessage({
|
||||
required String data,
|
||||
String? toUserID,
|
||||
String? groupID,
|
||||
String? description,
|
||||
String? extension,
|
||||
String desc = "",
|
||||
String extension = "",
|
||||
}) async {
|
||||
// 校验逻辑:单聊或群聊,二选一
|
||||
if ((toUserID == null && groupID == null) || (toUserID != null && groupID != null)) {
|
||||
return ImResult(
|
||||
success: false,
|
||||
code: -1,
|
||||
desc: "只能指定一个 receiver(toUserID)或 groupID",
|
||||
);
|
||||
}
|
||||
|
||||
// 1. 创建自定义消息
|
||||
final createRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().createCustomMessage(
|
||||
final res = await TencentImSDKPlugin.v2TIMManager.getMessageManager().createCustomMessage(
|
||||
data: data,
|
||||
desc: description ?? '',
|
||||
extension: extension ?? '',
|
||||
);
|
||||
|
||||
if (createRes.code != 0 || createRes.data?.id == null) {
|
||||
return ImResult(
|
||||
success: false,
|
||||
code: createRes.code,
|
||||
desc: "创建自定义消息失败",
|
||||
);
|
||||
}
|
||||
final V2TimMessage? messageInfo = createRes.data?.messageInfo;
|
||||
|
||||
// 2. 发送消息
|
||||
final sendRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage(
|
||||
message: messageInfo,
|
||||
receiver: toUserID ?? '',
|
||||
groupID: groupID ?? '',
|
||||
priority: MessagePriorityEnum.V2TIM_PRIORITY_DEFAULT,
|
||||
onlineUserOnly: false,
|
||||
isExcludedFromUnreadCount: false,
|
||||
isExcludedFromLastMessage: false,
|
||||
needReadReceipt: false,
|
||||
offlinePushInfo: OfflinePushInfo(
|
||||
title: "自定义消息",
|
||||
desc: description ?? '您收到一条自定义消息',
|
||||
),
|
||||
cloudCustomData: "",
|
||||
localCustomData: "",
|
||||
);
|
||||
|
||||
return ImResult(
|
||||
success: sendRes.code == 0,
|
||||
code: sendRes.code,
|
||||
desc: sendRes.desc,
|
||||
desc: desc,
|
||||
extension: extension,
|
||||
);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
/// 构造单聊伪消息
|
||||
@ -162,24 +129,124 @@ class IMMessage {
|
||||
desc: "success",
|
||||
data: timeMsg,
|
||||
);
|
||||
}
|
||||
|
||||
// final sendRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage(
|
||||
// message: timeMsg,
|
||||
// receiver: userId,
|
||||
// groupID: "",
|
||||
// onlineUserOnly: false,
|
||||
// isExcludedFromUnreadCount: true,
|
||||
// isExcludedFromLastMessage: true,
|
||||
// needReadReceipt: false,
|
||||
// cloudCustomData: "",
|
||||
// localCustomData: "time_label",
|
||||
// );
|
||||
/// 创建文本消息==1
|
||||
Future<ImResult<V2TimMsgCreateInfoResult>> createTextMessage({
|
||||
required String text,
|
||||
}) async {
|
||||
final res = await TencentImSDKPlugin.v2TIMManager.getMessageManager().createTextMessage(text: text);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
// return ImResult(
|
||||
// success: sendRes.code == 0,
|
||||
// code: sendRes.code,
|
||||
// desc: sendRes.code == 0 ? "时间标签发送成功" : sendRes.desc,
|
||||
// data: timeMsg,
|
||||
// );
|
||||
/// 创建图片消息==3
|
||||
Future<ImResult<V2TimMsgCreateInfoResult>> createImageMessage({
|
||||
required String imagePath,
|
||||
String? imageName,
|
||||
}) async {
|
||||
final fileExists = await File(imagePath).exists();
|
||||
if (fileExists) {
|
||||
final res = await TencentImSDKPlugin.v2TIMManager.getMessageManager().createImageMessage(
|
||||
imagePath: imagePath,
|
||||
imageName: imageName,
|
||||
);
|
||||
return ImResult.wrap(res);
|
||||
} else {
|
||||
// 构造失败的回调
|
||||
final failed = V2TimValueCallback<V2TimMsgCreateInfoResult>.fromJson({
|
||||
"code": -5,
|
||||
"desc": "imagePath is not found",
|
||||
"data": V2TimMsgCreateInfoResult.fromJson({}),
|
||||
});
|
||||
|
||||
return ImResult.wrap(failed);
|
||||
}
|
||||
}
|
||||
|
||||
///创建视频消息==5
|
||||
Future<ImResult<V2TimMsgCreateInfoResult>> createVideoMessage({
|
||||
//最大100MB
|
||||
required String videoFilePath, //视频地址
|
||||
required String type, // 类型mp4/avi==
|
||||
required int duration, // 时长
|
||||
required String snapshotPath, // 封面图
|
||||
}) async {
|
||||
final videoExists = await File(videoFilePath).exists();
|
||||
final snapshotExists = await File(snapshotPath).exists();
|
||||
|
||||
if (videoExists && snapshotExists) {
|
||||
final res = await TencentImSDKPlugin.v2TIMManager.getMessageManager().createVideoMessage(
|
||||
videoFilePath: videoFilePath,
|
||||
type: type,
|
||||
duration: duration,
|
||||
snapshotPath: snapshotPath,
|
||||
);
|
||||
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
// 构造失败回调
|
||||
final failed = V2TimValueCallback<V2TimMsgCreateInfoResult>.fromJson({
|
||||
"code": -5,
|
||||
"desc": "视频或首帧图缺失",
|
||||
"data": V2TimMsgCreateInfoResult.fromJson({}),
|
||||
});
|
||||
|
||||
return ImResult.wrap(failed);
|
||||
}
|
||||
|
||||
/// 语音消息==4
|
||||
Future<ImResult<V2TimMsgCreateInfoResult>> createSoundMessage({
|
||||
required String soundPath,
|
||||
required int duration,
|
||||
String? path,
|
||||
}) async {
|
||||
final soundExists = await File(soundPath).exists();
|
||||
|
||||
if (soundExists) {
|
||||
final res = await TencentImSDKPlugin.v2TIMManager.getMessageManager().createSoundMessage(
|
||||
soundPath: soundPath,
|
||||
duration: duration,
|
||||
);
|
||||
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
final failed = V2TimValueCallback<V2TimMsgCreateInfoResult>.fromJson({
|
||||
"code": -5,
|
||||
"desc": "音频文件缺失",
|
||||
"data": V2TimMsgCreateInfoResult.fromJson({}),
|
||||
});
|
||||
|
||||
return ImResult.wrap(failed);
|
||||
}
|
||||
|
||||
/// 表情 == 8
|
||||
Future<ImResult<V2TimMsgCreateInfoResult>> createFaceMessage({
|
||||
required int index,
|
||||
required String data,
|
||||
}) async {
|
||||
final res = await TencentImSDKPlugin.v2TIMManager.getMessageManager().createFaceMessage(
|
||||
index: index,
|
||||
data: data,
|
||||
);
|
||||
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
///相当于一个被禁用了网络发送能力的 sendMessage() 接口
|
||||
Future<ImResult<V2TimMessage>> insertC2CMessageToLocalStorageV2({
|
||||
required String userID,
|
||||
required String senderID,
|
||||
V2TimMessage? message,
|
||||
String? createdMsgID,
|
||||
}) async {
|
||||
final res = await TencentImSDKPlugin.v2TIMManager.getMessageManager().insertC2CMessageToLocalStorageV2(
|
||||
userID: userID,
|
||||
senderID: senderID,
|
||||
message: message,
|
||||
createdMsgID: createdMsgID,
|
||||
);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import 'package:loopin/IM/im_service.dart';
|
||||
import 'package:loopin/utils/index.dart';
|
||||
import 'package:loopin/utils/lifecycle_handler.dart';
|
||||
import 'package:loopin/utils/notification_banner.dart';
|
||||
import 'package:shirne_dialog/shirne_dialog.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
|
||||
@ -15,14 +16,18 @@ import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_receipt.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
|
||||
|
||||
class ImMessageListenerService extends GetxService {
|
||||
ProgressController? _sendProgressController; // 消息发送提示
|
||||
|
||||
final logger = Logger();
|
||||
V2TimAdvancedMsgListener? _listener;
|
||||
Timer? _debounceTimer;
|
||||
|
||||
/// 插入标签时间间隔
|
||||
bool needInsertTimeLabel(int lastTimestamp, int newTimestamp, {int interval = 3 * 60}) {
|
||||
return (newTimestamp - lastTimestamp) > interval * 1000;
|
||||
}
|
||||
|
||||
///插入标签
|
||||
void insertTimeLabel(message) async {
|
||||
// 待插入的消息
|
||||
List<V2TimMessage> messagesToInsert = [];
|
||||
@ -54,21 +59,26 @@ class ImMessageListenerService extends GetxService {
|
||||
messagesToInsert.add(resMsg.data);
|
||||
}
|
||||
messagesToInsert.insert(0, message);
|
||||
// messagesToInsert.add(message);
|
||||
|
||||
// 新进入的消息插入列表
|
||||
chatDetailController.chatList.insertAll(0, messagesToInsert);
|
||||
// chatDetailController.chatList.addAll(messagesToInsert);
|
||||
// 滚动
|
||||
chatDetailController.scrollToBottom();
|
||||
}
|
||||
|
||||
/// 处理消息
|
||||
void _handleNewMessage(V2TimMessage message) async {
|
||||
final userID = message.sender ?? '';
|
||||
if (userID.isEmpty) return;
|
||||
|
||||
// 是否正在聊天 优先处理
|
||||
if (Get.currentRoute == '/chat' && Get.isRegistered<ChatDetailController>()) {
|
||||
/// 是否正在聊天 优先处理
|
||||
if ((Get.currentRoute == '/chat' || Get.currentRoute == '/chatNoFriend' || Get.currentRoute == '/chatGroup') && Get.isRegistered<ChatDetailController>()) {
|
||||
final chatDetailController = Get.find<ChatDetailController>();
|
||||
// 单聊的处理
|
||||
if (chatDetailController.userID == userID) {
|
||||
// 确认正在聊天
|
||||
// 插入消息前检测是否需要打时间标签
|
||||
// 确认正在聊天,插入消息前检测是否需要打时间标签
|
||||
insertTimeLabel(message);
|
||||
// 标注为已读
|
||||
await ImService.instance.clearConversationUnreadCount(conversationID: 'c2c_$userID');
|
||||
@ -142,9 +152,29 @@ class ImMessageListenerService extends GetxService {
|
||||
},
|
||||
onRecvMessageModified: (V2TimMessage message) {
|
||||
logger.i("消息被修改: ${message.msgID}");
|
||||
// 目前就红包领取状态的变更
|
||||
if ((Get.currentRoute == '/chat' || Get.currentRoute == '/chatNoFriend' || Get.currentRoute == '/chatGroup') &&
|
||||
Get.isRegistered<ChatDetailController>()) {
|
||||
final controller = Get.find<ChatDetailController>();
|
||||
final index = controller.chatList.indexWhere((m) => m.msgID == message.msgID);
|
||||
if (index != -1) {
|
||||
final newJson = message.customElem!.data!;
|
||||
controller.chatList[index].customElem!.data = newJson;
|
||||
controller.chatList.refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
onSendMessageProgress: (V2TimMessage message, int progress) {
|
||||
logger.i("发送中: ${message.msgID} -> $progress%");
|
||||
if (progress < 100) {
|
||||
_sendProgressController ??= MyDialog.loading(
|
||||
"发送中...",
|
||||
duration: null, // 不自动关闭
|
||||
);
|
||||
} else {
|
||||
_sendProgressController?.close();
|
||||
_sendProgressController = null;
|
||||
}
|
||||
},
|
||||
onRecvC2CReadReceipt: (List<V2TimMessageReceipt> receiptList) {
|
||||
for (var receipt in receiptList) {
|
||||
@ -159,7 +189,6 @@ class ImMessageListenerService extends GetxService {
|
||||
);
|
||||
|
||||
TencentImSDKPlugin.v2TIMManager.getMessageManager().addAdvancedMsgListener(listener: _listener!);
|
||||
logger.i("$_listener");
|
||||
|
||||
logger.i("高级消息监听器已注册");
|
||||
return this;
|
||||
|
@ -1,3 +1,6 @@
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart';
|
||||
|
||||
class ImResult<T> {
|
||||
final bool success;
|
||||
final int code;
|
||||
@ -10,4 +13,21 @@ class ImResult<T> {
|
||||
required this.desc,
|
||||
this.data,
|
||||
});
|
||||
static ImResult<T> wrap<T>(V2TimValueCallback<T> res) {
|
||||
return ImResult(
|
||||
success: res.code == 0,
|
||||
code: res.code,
|
||||
desc: res.desc,
|
||||
data: res.data,
|
||||
);
|
||||
}
|
||||
|
||||
static ImResult<void> wrapNoData(V2TimCallback res) {
|
||||
return ImResult<void>(
|
||||
success: res.code == 0,
|
||||
code: res.code,
|
||||
desc: res.desc,
|
||||
data: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:loopin/IM/controller/chat_controller.dart';
|
||||
import 'package:loopin/IM/controller/im_user_info_controller.dart';
|
||||
import 'package:loopin/IM/controller/tab_bar_controller.dart';
|
||||
import 'package:loopin/IM/global_badge.dart';
|
||||
import 'package:loopin/IM/im_core.dart';
|
||||
import 'package:loopin/IM/im_friend_listeners.dart';
|
||||
import 'package:loopin/IM/im_message_listeners.dart';
|
||||
import 'package:loopin/IM/im_result.dart';
|
||||
import 'package:loopin/IM/push_service.dart';
|
||||
import 'package:loopin/models/conversation_view_model.dart';
|
||||
import 'package:loopin/utils/wxsdk.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/friend_application_type_enum.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/friend_response_type_enum.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/friend_type_enum.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/history_msg_get_type_enum.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_filter.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_operation_result.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_result.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_follow_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_follow_operation_result.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_follow_type_check_result.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_info_result.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_operation_result.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_change_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_info_result.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/native_im/adapter/tim_conversation_manager.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/native_im/adapter/tim_friendship_manager.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/native_im/adapter/tim_message_manager.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
|
||||
|
||||
final logger = Logger();
|
||||
@ -40,6 +61,16 @@ class ImService {
|
||||
|
||||
if (result.success) {
|
||||
logger.i("IM 登录成功:$userID");
|
||||
// 初始化push服务
|
||||
PushService().initPush(
|
||||
sdkAppId: 1600080789,
|
||||
appKey: 'vkFpe55aYqfV7Sk5uGaoxhEstJ3tcI9dquk7JwG1GloDSLD2HeMWeQweWWXgNlhC',
|
||||
);
|
||||
// 初始化微信 SDK
|
||||
await Wxsdk.init();
|
||||
|
||||
// 注册用户信息(基本信息+自定义信息)
|
||||
Get.put(ImUserInfoController(), permanent: true);
|
||||
// 登录成功后注册高级消息监听器
|
||||
final messageService = ImMessageListenerService();
|
||||
Get.put<ImMessageListenerService>(messageService, permanent: true);
|
||||
@ -48,13 +79,11 @@ class ImService {
|
||||
|
||||
// 注册关系链监听器
|
||||
final friendListener = ImFriendListeners();
|
||||
logger.i(friendListener);
|
||||
Get.put<ImFriendListeners>(friendListener, permanent: true);
|
||||
friendListener.register();
|
||||
|
||||
/// 注册消息未读数监听器
|
||||
Get.put(GlobalBadge(), permanent: true);
|
||||
// Get.lazyPut<GlobalBadge>(() => GlobalBadge());
|
||||
} else {
|
||||
logger.i("IM 登录失败:${result.code} - ${result.desc}");
|
||||
Get.snackbar(
|
||||
@ -72,6 +101,9 @@ class ImService {
|
||||
Future<ImResult> logout() async {
|
||||
final res = await TencentImSDKPlugin.v2TIMManager.logout();
|
||||
if (res.code == 0) {
|
||||
/// 清理用户信息
|
||||
Get.delete<ImUserInfoController>(force: true);
|
||||
|
||||
/// 移出消息监听器
|
||||
Get.find<ImMessageListenerService>().onClose();
|
||||
Get.delete<ImMessageListenerService>(force: true);
|
||||
@ -83,25 +115,114 @@ class ImService {
|
||||
/// 清理tabbar
|
||||
Get.find<TabBarController>().badgeMap.clear();
|
||||
|
||||
/// 清理会话列表数据
|
||||
Get.find<ChatController>().initChatData();
|
||||
|
||||
/// 移出未读消息监听器
|
||||
Get.find<GlobalBadge>().onClose();
|
||||
Get.delete<GlobalBadge>(force: true);
|
||||
|
||||
/// 移除推送服务
|
||||
PushService.unInitPush();
|
||||
|
||||
/// 反初始化
|
||||
ImCore.unInit();
|
||||
}
|
||||
return ImResult(
|
||||
success: res.code == 0,
|
||||
code: res.code,
|
||||
desc: res.desc,
|
||||
return ImResult.wrapNoData(res);
|
||||
}
|
||||
|
||||
/// 设置会话自定义属性
|
||||
Future<ImResult<List<V2TimConversationOperationResult>>> setConversationCustomData({
|
||||
required String customData,
|
||||
required List<String> conversationIDList,
|
||||
}) async {
|
||||
final res = await TIMConversationManager.instance.setConversationCustomData(customData: customData, conversationIDList: conversationIDList);
|
||||
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
/// 获取符合过滤条件的未读消息总数
|
||||
Future<ImResult<int>> getUnreadMessageCountByFilter({
|
||||
required V2TimConversationFilter filter,
|
||||
}) async {
|
||||
final res = await TIMConversationManager.instance.getUnreadMessageCountByFilter(
|
||||
filter: filter,
|
||||
);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
/// 删除会话
|
||||
Future<ImResult<void>> deleteConversation({
|
||||
required String conversationID,
|
||||
}) async {
|
||||
final res = await TIMConversationManager.instance.deleteConversation(conversationID: conversationID);
|
||||
|
||||
return ImResult.wrapNoData(res);
|
||||
}
|
||||
|
||||
/// 创建会话分组
|
||||
Future<ImResult<List<V2TimConversationOperationResult>>> createConversationGroup({
|
||||
required String groupName,
|
||||
required List<String> conversationIDList,
|
||||
}) async {
|
||||
final res = await TIMConversationManager.instance.createConversationGroup(
|
||||
groupName: groupName,
|
||||
conversationIDList: conversationIDList,
|
||||
);
|
||||
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
/// 获取会话分组列表
|
||||
Future<ImResult<List<String>>> getConversationGroupList() async {
|
||||
final res = await TIMConversationManager.instance.getConversationGroupList();
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
/// 将会话添加到分组
|
||||
Future<ImResult<List<V2TimConversationOperationResult>>> addConversationsToGroup({
|
||||
required String groupName,
|
||||
required List<String> conversationIDList,
|
||||
}) async {
|
||||
final res = await TIMConversationManager.instance.addConversationsToGroup(
|
||||
groupName: groupName,
|
||||
conversationIDList: conversationIDList,
|
||||
);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
///将会话移除分组
|
||||
Future<ImResult<List<V2TimConversationOperationResult>>> deleteConversationsFromGroup({
|
||||
required String groupName,
|
||||
required List<String> conversationIDList,
|
||||
}) async {
|
||||
final res = await TIMConversationManager.instance.deleteConversationsFromGroup(
|
||||
groupName: groupName,
|
||||
conversationIDList: conversationIDList,
|
||||
);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
/// 高级查询会话列表
|
||||
Future<ImResult<V2TimConversationResult>> getConversationListByFilter({
|
||||
required V2TimConversationFilter filter,
|
||||
required int nextSeq,
|
||||
int count = 20,
|
||||
}) async {
|
||||
final res = await TIMConversationManager.instance.getConversationListByFilter(
|
||||
filter: filter,
|
||||
nextSeq: nextSeq,
|
||||
count: count,
|
||||
);
|
||||
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
/// 查询会话记录
|
||||
Future<ImResult> getConversationList(String nextSeq, int count) async {
|
||||
final res = await TencentImSDKPlugin.v2TIMManager.getConversationManager().getConversationList(nextSeq: nextSeq, count: count);
|
||||
|
||||
if (res.code != 0) {
|
||||
// final res = await TencentImSDKPlugin.v2TIMManager.getConversationManager().getConversationList(nextSeq: nextSeq, count: count);
|
||||
final res = await getConvData(nextSeq, count);
|
||||
if (res.success == false) {
|
||||
return ImResult(
|
||||
success: false,
|
||||
code: res.code,
|
||||
@ -138,7 +259,7 @@ class ImService {
|
||||
// 读取管理员标识
|
||||
final customInfo = user.customInfo;
|
||||
if (customInfo != null) {
|
||||
isCustomAdmin = customInfo['Tag_Profile_Custom_admin'] ?? '0';
|
||||
isCustomAdmin = customInfo['admin'] ?? '0';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -171,6 +292,40 @@ class ImService {
|
||||
return ConversationViewModel(conversation: conv, faceUrl: faceUrl, isCustomAdmin: isCustomAdmin);
|
||||
}).toList();
|
||||
|
||||
// 筛选数据,过滤掉陌生人消息
|
||||
viewList.removeWhere((conv) {
|
||||
final special = conv.conversation.conversationGroupList ?? [];
|
||||
return special.contains('noFriend');
|
||||
});
|
||||
|
||||
ChatController chatcontroller = Get.find<ChatController>();
|
||||
logger.e('新的分页内容:${res.data!.toLogString()},控制器中的:${chatcontroller.nextSeq.value}');
|
||||
String newNextSeq = res.data?.nextSeq ?? '0';
|
||||
bool isEnd = res.data?.isFinished ?? true;
|
||||
|
||||
if (isEnd) {
|
||||
//没数据了,关闭拉取;
|
||||
chatcontroller.isFinished.value = isEnd;
|
||||
} else {
|
||||
// 没拉完,记录游标
|
||||
chatcontroller.nextSeq.value = newNextSeq;
|
||||
}
|
||||
|
||||
// 更新分页
|
||||
chatcontroller.nextSeq.value = res.data!.nextSeq!;
|
||||
if (res.data!.isFinished == false) {
|
||||
if (viewList.length < 20) {
|
||||
// 递归补偿拉取
|
||||
final nextRes = await getConversationList(
|
||||
res.data!.nextSeq!,
|
||||
count,
|
||||
);
|
||||
if (nextRes.success && nextRes.data != null) {
|
||||
viewList.addAll(nextRes.data as List<ConversationViewModel>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ImResult(
|
||||
success: res.code == 0,
|
||||
code: res.code,
|
||||
@ -179,30 +334,13 @@ class ImService {
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取自己的userId
|
||||
Future<ImResult> selfUserId() async {
|
||||
V2TimValueCallback<String> self = await TencentImSDKPlugin.v2TIMManager.getLoginUser();
|
||||
String? userId = self.data;
|
||||
return ImResult(
|
||||
success: self.code == 0,
|
||||
code: self.code,
|
||||
desc: self.desc,
|
||||
data: userId,
|
||||
);
|
||||
}
|
||||
|
||||
/// 查询当前登录用户的个人信息
|
||||
Future<ImResult> selfInfo() async {
|
||||
// 获取当前登录的用户 ID
|
||||
final idRes = await selfUserId();
|
||||
// 获取用户信息
|
||||
V2TimValueCallback<List<V2TimUserFullInfo>> res = await TencentImSDKPlugin.v2TIMManager.getUsersInfo(userIDList: [idRes.data]);
|
||||
return ImResult(
|
||||
success: res.code == 0,
|
||||
code: res.code,
|
||||
desc: res.desc,
|
||||
data: res.data?.isNotEmpty == true ? res.data![0] : null,
|
||||
);
|
||||
///获取所有会话数据
|
||||
Future<ImResult<V2TimConversationResult>> getConvData(String nextSeq, int count) async {
|
||||
final res = await TencentImSDKPlugin.v2TIMManager.getConversationManager().getConversationList(nextSeq: nextSeq, count: count);
|
||||
// for (var element in res.data!.conversationList) {
|
||||
// logger.e('所有的会话数据:${element.toJson()}');
|
||||
// }
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
///获取指定会话
|
||||
@ -219,13 +357,29 @@ class ImService {
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取消息
|
||||
Future<ImResult<List<V2TimMessage>>> findMessages({
|
||||
required List<String> messageIDList,
|
||||
}) async {
|
||||
final res = await TIMMessageManager.instance.findMessages(messageIDList: messageIDList);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
/// 修改消息
|
||||
Future<ImResult<V2TimMessageChangeInfo>> modifyMessage({
|
||||
required V2TimMessage message,
|
||||
}) async {
|
||||
final res = await TIMMessageManager.instance.modifyMessage(message: message);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
/// 获取聊天记录 如果是群聊传 groupID,单聊传 userID,二选一
|
||||
Future<ImResult<List<V2TimMessage>>> getHistoryMessageList({
|
||||
HistoryMsgGetTypeEnum getType = HistoryMsgGetTypeEnum.V2TIM_GET_LOCAL_OLDER_MSG,
|
||||
String? userID,
|
||||
String? groupID,
|
||||
int? lastMsgSeq,
|
||||
int count = 20,
|
||||
int count = 10,
|
||||
V2TimMessage? lastMsg,
|
||||
List<int>? messageTypeList,
|
||||
List<int>? messageSeqList,
|
||||
@ -298,10 +452,223 @@ class ImService {
|
||||
cleanSequence: cleanSequence, // 群聊生效
|
||||
);
|
||||
|
||||
return ImResult.wrapNoData(res);
|
||||
}
|
||||
|
||||
/// 获取自己的userId
|
||||
Future<ImResult> selfUserId() async {
|
||||
V2TimValueCallback<String> self = await TencentImSDKPlugin.v2TIMManager.getLoginUser();
|
||||
String? userId = self.data;
|
||||
return ImResult(
|
||||
success: self.code == 0,
|
||||
code: self.code,
|
||||
desc: self.desc,
|
||||
data: userId,
|
||||
);
|
||||
}
|
||||
|
||||
/// 查询当前登录用户的个人信息
|
||||
Future<ImResult> selfInfo() async {
|
||||
// 获取当前登录的用户 ID
|
||||
final idRes = await selfUserId();
|
||||
// 获取用户信息
|
||||
V2TimValueCallback<List<V2TimUserFullInfo>> res = await TencentImSDKPlugin.v2TIMManager.getUsersInfo(userIDList: [idRes.data]);
|
||||
return ImResult(
|
||||
success: res.code == 0,
|
||||
code: res.code,
|
||||
desc: res.desc,
|
||||
data: res.data?.isNotEmpty == true ? res.data!.first : null,
|
||||
);
|
||||
}
|
||||
|
||||
/// 查询其他人的信息
|
||||
Future<ImResult> otherInfo(id) async {
|
||||
// 获取用户信息
|
||||
V2TimValueCallback<List<V2TimUserFullInfo>> res = await TencentImSDKPlugin.v2TIMManager.getUsersInfo(userIDList: [id]);
|
||||
return ImResult(
|
||||
success: res.code == 0,
|
||||
code: res.code,
|
||||
desc: res.desc,
|
||||
data: res.data?.isNotEmpty == true ? res.data!.first : null,
|
||||
);
|
||||
}
|
||||
|
||||
/// 设置个人资料
|
||||
Future<ImResult> setSelfInfo({
|
||||
required V2TimUserFullInfo userFullInfo,
|
||||
}) async {
|
||||
final res = await TencentImSDKPlugin.v2TIMManager.setSelfInfo(
|
||||
userFullInfo: userFullInfo,
|
||||
);
|
||||
return ImResult.wrapNoData(res);
|
||||
}
|
||||
|
||||
/// 检查是否是好友(双向或单向)
|
||||
Future<ImResult> isMyFriend(String userID, FriendTypeEnum checkType) async {
|
||||
final res = await TIMFriendshipManager.instance.checkFriend(
|
||||
userIDList: [userID],
|
||||
checkType: checkType, //V2TIM_FRIEND_TYPE_BOTH V2TIM_FRIEND_TYPE_SINGLE
|
||||
);
|
||||
|
||||
if (res.code == 0 && res.data != null && res.data!.isNotEmpty) {
|
||||
final resultType = res.data!.first.resultType;
|
||||
final isFriend = resultType == 3; //0=无, 1=单向, 2=我在对方列表,3=双向
|
||||
return ImResult(
|
||||
success: true,
|
||||
desc: res.desc,
|
||||
code: res.code,
|
||||
data: isFriend,
|
||||
);
|
||||
} else {
|
||||
return ImResult(
|
||||
success: false,
|
||||
code: res.code,
|
||||
desc: res.desc,
|
||||
data: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加好友
|
||||
Future<ImResult<V2TimFriendOperationResult>> addFriend({
|
||||
required String userID,
|
||||
String? remark,
|
||||
String? friendGroup,
|
||||
String? addWording,
|
||||
String? addSource,
|
||||
required FriendTypeEnum addType,
|
||||
}) async {
|
||||
final res = await TIMFriendshipManager.instance.addFriend(
|
||||
userID: userID,
|
||||
remark: remark,
|
||||
friendGroup: friendGroup,
|
||||
addWording: addWording,
|
||||
addSource: addSource,
|
||||
addType: addType,
|
||||
);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
///接受好友申请
|
||||
Future<ImResult<V2TimFriendOperationResult>> acceptFriendApplication({
|
||||
required FriendResponseTypeEnum responseType,
|
||||
required FriendApplicationTypeEnum type, // V2TIM_FRIEND_ACCEPT_AGREE,同意添加单向好友;V2TIM_FRIEND_ACCEPT_AGREE_AND_ADD,同意并添加为双向好友
|
||||
required String userID,
|
||||
}) async {
|
||||
final res = await TIMFriendshipManager.instance.acceptFriendApplication(
|
||||
responseType: responseType,
|
||||
type: type,
|
||||
userID: userID,
|
||||
);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
/// 拉黑
|
||||
Future<ImResult<List<V2TimFriendOperationResult>>> addToBlackList({
|
||||
required List<String> userIDList,
|
||||
}) async {
|
||||
final res = await TIMFriendshipManager.instance.addToBlackList(userIDList: userIDList);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
/// 取消拉黑
|
||||
Future<ImResult<List<V2TimFriendOperationResult>>> deleteFromBlackList({
|
||||
required List<String> userIDList,
|
||||
}) async {
|
||||
final res = await TIMFriendshipManager.instance.deleteFromBlackList(userIDList: userIDList);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
///获取好友列表
|
||||
Future<ImResult<List<V2TimFriendInfo>>> getFriendList() async {
|
||||
final res = await TIMFriendshipManager.instance.getFriendList();
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
/// set好友备注
|
||||
Future<ImResult> setFriendInfo({
|
||||
required String userID,
|
||||
String? friendRemark,
|
||||
Map<String, String>? friendCustomInfo,
|
||||
}) async {
|
||||
late V2TimCallback res;
|
||||
res = await TIMFriendshipManager.instance.setFriendInfo(
|
||||
userID: userID,
|
||||
friendRemark: friendRemark,
|
||||
friendCustomInfo: friendCustomInfo,
|
||||
);
|
||||
return ImResult.wrapNoData(res);
|
||||
}
|
||||
|
||||
/// 获取好友信息
|
||||
Future<ImResult<List<V2TimFriendInfoResult>>> getFriendInfo({
|
||||
required List<String> userIDList,
|
||||
}) async {
|
||||
final res = await TIMFriendshipManager.instance.getFriendsInfo(userIDList: userIDList);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
///关注
|
||||
Future<ImResult<List<V2TimFollowOperationResult>>> followUser({
|
||||
required List<String> userIDList,
|
||||
}) async {
|
||||
final res = await TIMFriendshipManager.instance.followUser(
|
||||
userIDList: userIDList,
|
||||
);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
///取关
|
||||
Future<ImResult<List<V2TimFollowOperationResult>>> unfollowUser({
|
||||
required List<String> userIDList,
|
||||
}) async {
|
||||
final res = await TIMFriendshipManager.instance.unfollowUser(
|
||||
userIDList: userIDList,
|
||||
);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
/// check关注的类型
|
||||
/// 0:不是好友也没有关注
|
||||
/// 1:你关注了对方(单向)
|
||||
/// 2:对方关注了你(单向)
|
||||
/// 3:互相关注(双向好友)
|
||||
Future<ImResult<List<V2TimFollowTypeCheckResult>>> checkFollowType({
|
||||
required List<String> userIDList,
|
||||
}) async {
|
||||
final res = await TIMFriendshipManager.instance.checkFollowType(
|
||||
userIDList: userIDList,
|
||||
);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
///获取指定用户的 关注/粉丝/互关 数量信息
|
||||
Future<ImResult<List<V2TimFollowInfo>>> getUserFollowInfo({
|
||||
required List<String> userIDList,
|
||||
}) async {
|
||||
final res = await TIMFriendshipManager.instance.getUserFollowInfo(userIDList: userIDList);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
/// 获取双向关注列表(互关好友)
|
||||
/// [nextCursor] 分页游标,首次传空字符串
|
||||
Future<ImResult<V2TimUserInfoResult>> getMutualFollowersList({
|
||||
required String nextCursor,
|
||||
}) async {
|
||||
final res = await TIMFriendshipManager.instance.getMutualFollowersList(
|
||||
nextCursor: nextCursor,
|
||||
);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
|
||||
/// 获取我的粉丝列表
|
||||
/// [nextCursor] 分页游标,首次传空字符串
|
||||
Future<ImResult<V2TimUserInfoResult>> getMyFollowersList({
|
||||
required String nextCursor,
|
||||
}) async {
|
||||
final res = await TIMFriendshipManager.instance.getMyFollowersList(
|
||||
nextCursor: nextCursor,
|
||||
);
|
||||
return ImResult.wrap(res);
|
||||
}
|
||||
}
|
||||
|
208
lib/IM/push_service.dart
Normal file
@ -0,0 +1,208 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:loopin/IM/im_service.dart';
|
||||
import 'package:loopin/models/conversation_type.dart';
|
||||
import 'package:loopin/utils/storage.dart';
|
||||
import 'package:tencent_cloud_chat_push/common/tim_push_listener.dart';
|
||||
import 'package:tencent_cloud_chat_push/common/tim_push_message.dart';
|
||||
import 'package:tencent_cloud_chat_push/tencent_cloud_chat_push.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
|
||||
|
||||
final logger = Logger();
|
||||
|
||||
class PushService {
|
||||
static late TIMPushListener _timPushListener;
|
||||
|
||||
Future<void> _registerPushInIsolate(Map<String, dynamic> args) async {
|
||||
final sdkAppId = args['sdkAppId'] as int;
|
||||
final appKey = args['appKey'] as String;
|
||||
final apnsCertificateID = args['apnsCertificateID'] as int;
|
||||
|
||||
try {
|
||||
await TencentCloudChatPush().registerPush(
|
||||
sdkAppId: sdkAppId,
|
||||
appKey: appKey,
|
||||
apnsCertificateID: apnsCertificateID,
|
||||
onNotificationClicked: _onNotificationClicked,
|
||||
);
|
||||
} catch (e) {
|
||||
logger.e('注册推送失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化推送服务
|
||||
Future<void> initPush({
|
||||
required int sdkAppId,
|
||||
required String appKey, // 客户端密钥
|
||||
}) async {
|
||||
int apnsCertificateID;
|
||||
final devices = await _getDeviceBrand();
|
||||
apnsCertificateID = _getApnsCertificateIDForBrand(devices);
|
||||
if (apnsCertificateID == 0) {
|
||||
logger.w('手机厂商:$devices, 未配置证书');
|
||||
}
|
||||
|
||||
// 注册推送(初始化)
|
||||
// if (Platform.isAndroid) {
|
||||
// await compute(_registerPushInIsolate, {
|
||||
// 'sdkAppId': sdkAppId,
|
||||
// 'appKey': appKey,
|
||||
// 'apnsCertificateID': apnsCertificateID,
|
||||
// });
|
||||
// } else {
|
||||
await TencentCloudChatPush().registerPush(
|
||||
onNotificationClicked: _onNotificationClicked,
|
||||
sdkAppId: sdkAppId,
|
||||
appKey: appKey,
|
||||
apnsCertificateID: apnsCertificateID,
|
||||
);
|
||||
// }
|
||||
|
||||
// 关闭 App 在前台时弹出通知栏
|
||||
await TencentCloudChatPush().disablePostNotificationInForeground(disable: true);
|
||||
|
||||
///处理安卓端异常问题;
|
||||
if (Platform.isAndroid) {
|
||||
await TencentImSDKPlugin.v2TIMManager.login(userID: Storage.read('userId'), userSig: Storage.read('userSig'));
|
||||
}
|
||||
|
||||
logger.i('推送服务已注册,手机:$devices,证书ID:$apnsCertificateID');
|
||||
|
||||
// 添加在线时监听器
|
||||
_addPushListener();
|
||||
}
|
||||
|
||||
/// 注销推送(退出登录时调用)
|
||||
static Future<void> unInitPush() async {
|
||||
try {
|
||||
await TencentCloudChatPush().unRegisterPush();
|
||||
_removePushListener();
|
||||
} catch (e) {
|
||||
logger.i("注销推送失败: $e");
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加监听器
|
||||
static void _addPushListener() {
|
||||
_timPushListener = TIMPushListener(
|
||||
onRecvPushMessage: (TimPushMessage message) {
|
||||
logger.i("[推送] 收到消息: ${message.toLogString()}");
|
||||
},
|
||||
onRevokePushMessage: (String messageId) {
|
||||
logger.i("[推送] 消息被撤回: $messageId");
|
||||
},
|
||||
onNotificationClicked: (String ext) {
|
||||
logger.i("[推送] 点击横幅 ext: $ext");
|
||||
_handleNotificationClick(ext);
|
||||
},
|
||||
);
|
||||
TencentCloudChatPush().addPushListener(listener: _timPushListener);
|
||||
logger.i('推送服务在线监听器已注册');
|
||||
}
|
||||
|
||||
/// 移除监听器
|
||||
static void _removePushListener() {
|
||||
TencentCloudChatPush().removePushListener(listener: _timPushListener);
|
||||
}
|
||||
|
||||
/// 横幅点击事件处理
|
||||
static void _onNotificationClicked({
|
||||
required String ext,
|
||||
String? userID,
|
||||
String? groupID,
|
||||
}) {
|
||||
logger.i("[点击通知回调] ext: $ext, userID: $userID, groupID: $groupID");
|
||||
_handleNotificationClick(ext, userID: userID, groupID: groupID);
|
||||
}
|
||||
|
||||
/// 统一处理跳转逻辑
|
||||
static void _handleNotificationClick(String ext, {String? userID, String? groupID}) async {
|
||||
try {
|
||||
// ext={id:对应业务ID,type:'newFoucs',userID:发送人的id,groupID:群ID}
|
||||
// final ext = jsonEncode({
|
||||
// "userID": "123456",
|
||||
// "groupID": "654321",
|
||||
// });
|
||||
final data = jsonDecode(ext);
|
||||
logger.i(data);
|
||||
final type = data['type'];
|
||||
final router = conversationTypeFromString(type);
|
||||
logger.w(router);
|
||||
if (router == null || router != '') {
|
||||
// 聊天
|
||||
if (data['userID'] != null) {
|
||||
logger.w('有userID');
|
||||
// 单聊,获取会话
|
||||
final covRes = await ImService.instance.getConversation(conversationID: 'c2c_${data['userID']}');
|
||||
final V2TimConversation conversation = covRes.data;
|
||||
logger.w(conversation.toJson());
|
||||
if (conversation.conversationGroupList?.contains(ConversationType.noFriend.name) ?? false) {
|
||||
// nofriend会话,是否第一次聊天
|
||||
conversation.showName = conversation.showName ?? data['title'];
|
||||
Get.toNamed('/chatNoFriend', arguments: conversation);
|
||||
} else {
|
||||
// 去正常的会话
|
||||
Get.toNamed('/chat', arguments: conversation);
|
||||
}
|
||||
} else {
|
||||
logger.w('没有userID');
|
||||
|
||||
// 群聊消息
|
||||
final groupRes = await ImService.instance.getConversation(conversationID: 'group_${data['groupID']}');
|
||||
Get.toNamed('/chatGroup', arguments: groupRes.data);
|
||||
}
|
||||
} else {
|
||||
// 通知类相关
|
||||
Get.toNamed('/$router', arguments: data['id'] ?? '');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.i("[推送点击] ext 解析失败: $e");
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取手机品牌
|
||||
static Future<String> _getDeviceBrand() async {
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
try {
|
||||
if (Platform.isAndroid) {
|
||||
final androidInfo = await deviceInfo.androidInfo;
|
||||
return androidInfo.brand.toLowerCase();
|
||||
} else if (Platform.isIOS) {
|
||||
return 'apple';
|
||||
} else {
|
||||
return 'unknown';
|
||||
}
|
||||
} catch (e, stack) {
|
||||
logger.w("获取设备品牌失败: $e\n$stack");
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取对应厂商的证书ID
|
||||
static int _getApnsCertificateIDForBrand(String brand) {
|
||||
switch (brand) {
|
||||
case 'xiaomi':
|
||||
case 'redmi':
|
||||
return 41169;
|
||||
case 'oppo':
|
||||
return 41170;
|
||||
case 'vivo':
|
||||
return 41177;
|
||||
case 'meizu':
|
||||
return 41176;
|
||||
case 'apple':
|
||||
return 45356;
|
||||
case 'huawei':
|
||||
return 41171;
|
||||
case 'honor':
|
||||
return 41178;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,15 @@
|
||||
class CommonApi {
|
||||
static const String checkVersion = '/check/version'; // 查询版本
|
||||
static const String getCode = '/resource/sms/code'; // 发送短信验证码
|
||||
static const String login = '/auth/login'; // 登录
|
||||
static const String uploadFile = '/upload'; // 上传文件
|
||||
static const String accountInfo = '/ums/member/account/'; // 账户信息
|
||||
///----------get
|
||||
static const String getCode = '/resource/sms/code'; // 发送短信验证码 {'phonenumber'}
|
||||
static const String accountInfo = '/app/member/info'; // 账户信息
|
||||
|
||||
///---------post
|
||||
static const String login = '/auth/login'; // 登录 {'phonenumber': '', 'smsCode': '', 'clientId': '428a8310cd442757ae699df5d894f051', 'grantType': 'sms'};
|
||||
static const String checkVersion = '/system/version/list'; // 查询app版本 {'platformType': Platform.isAndroid ? 'android' : 'ios','status': 1}
|
||||
static const String uploadFile = '/resource/oss/upload';
|
||||
|
||||
///[source]=wechat_open [clientId]=428a8310cd442757ae699df5d894f051 [grantType]=social [socialState]=1
|
||||
static const String wxLogin = '/app/member/bind/wechat';
|
||||
|
||||
///resource/oss/upload
|
||||
}
|
||||
|
17
lib/api/shop_api.dart
Normal file
@ -0,0 +1,17 @@
|
||||
class ShopApi {
|
||||
///---------------------post
|
||||
/// [size]分页数量
|
||||
/// [current] 第几页
|
||||
/// [categoryId] 分类id
|
||||
/// [nameLike] 商品名称
|
||||
static const String shopList = '/app/product/page'; // 商品列表
|
||||
|
||||
/// [showStatus]1=显示 [nameLike]分类名称
|
||||
static const String shopCategory = '/app/productCategory/page'; // 商品分类
|
||||
/// []
|
||||
static const String shopSwiperList = '/app/article/carousel'; // 商品首页轮播图
|
||||
|
||||
///---------------------get
|
||||
/// [url参数/id]
|
||||
static const String shopDetail = '/app/product'; // 商品详情
|
||||
}
|
@ -1,18 +1,19 @@
|
||||
class VideoApi {
|
||||
// get
|
||||
static const String vlogList = '/vlog/indexList'; // 推荐视频列表数据
|
||||
static const String myPublicList = '/vlog/myPublicList'; // 我发布的视频
|
||||
static const String myPrivateList = '/vlog/myPrivateList'; // 我的私密视频
|
||||
static const String myLikedList = '/vlog/myLikedList'; // 我点赞的视频
|
||||
static const String friendList = '/vlog/friendList'; //互关好友的视频
|
||||
static const String followList = '/vlog/followList'; // 我关注的博主视频
|
||||
static const String detail = '/vlog/detail'; // 视频详情
|
||||
static const String vlogList = '/app/vlog/indexList'; // 推荐视频列表数据
|
||||
static const String myPrivateList = '/app/vlog/myPrivateList'; // 我的私密视频
|
||||
static const String friendList = '/app/vlog/friendList'; //互关好友的视频
|
||||
static const String followList = '/app/vlog/followList'; // 我关注的博主视频
|
||||
static const String detail = '/app/vlog/detail'; // 视频详情
|
||||
// post
|
||||
static const String unlike = '/vlog/unlike'; //取消点赞
|
||||
static const String totalLikedCounts = '/vlog/totalLikedCounts'; //收到点赞总数
|
||||
static const String publish = '/vlog/publish'; //发布视频
|
||||
static const String like = '/vlog/like'; //点赞
|
||||
static const String changeVlogStatus = '/vlog/changeVlogStatus'; //修改我的视频状态(删除视频)
|
||||
static const String changeToPublic = '/vlog/changeToPublic'; //将视频改为公开状态
|
||||
static const String changeToPrivate = '/vlog/changeToPrivate'; //将视频改为私密状态
|
||||
static const String myPublicList = '/app/vlog/myPublicList'; // 我发布的视频
|
||||
static const String myLikedList = '/app/vlog/myLikedList'; // 我点赞的视频
|
||||
|
||||
static const String unlike = '/app/vlog/unlike'; //取消点赞
|
||||
static const String totalLikedCounts = '/app/vlog/totalLikedCounts'; //收到点赞总数
|
||||
static const String publish = '/app/vlog/publish'; //发布视频
|
||||
static const String like = '/app/vlog/like'; //点赞
|
||||
static const String changeVlogStatus = '/app/vlog/changeVlogStatus'; //修改我的视频状态(删除视频)
|
||||
static const String changeToPublic = '/app/vlog/changeToPublic'; //将视频改为公开状态
|
||||
static const String changeToPrivate = '/app/vlog/changeToPrivate'; //将视频改为私密状态
|
||||
}
|
||||
|
11
lib/bings/chat_binding.dart
Normal file
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:loopin/IM/controller/chat_detail_controller.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
|
||||
|
||||
class ChatBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
V2TimConversation conversation = Get.arguments;
|
||||
Get.put(ChatDetailController(userID: conversation.userID!));
|
||||
}
|
||||
}
|
@ -2,11 +2,20 @@
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
typedef OnPinnedChanged = void Function(bool pinned);
|
||||
|
||||
class CustomStickyHeader extends SliverPersistentHeaderDelegate {
|
||||
final PreferredSize child;
|
||||
RxBool? isPinned;
|
||||
RxDouble? positions;
|
||||
|
||||
CustomStickyHeader({required this.child});
|
||||
CustomStickyHeader({
|
||||
required this.child,
|
||||
this.isPinned,
|
||||
this.positions,
|
||||
});
|
||||
|
||||
@override
|
||||
double get minExtent => child.preferredSize.height;
|
||||
@ -21,6 +30,21 @@ class CustomStickyHeader extends SliverPersistentHeaderDelegate {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
// overlapsContent 或 shrinkOffset >= maxExtent - minExtent 都可以判断是否吸顶
|
||||
bool pinned = overlapsContent; // true 表示已经吸顶
|
||||
if (isPinned != null && isPinned!.value != pinned) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
isPinned!.value = pinned;
|
||||
});
|
||||
}
|
||||
if (positions != null) {
|
||||
if ((maxExtent - minExtent) >= shrinkOffset) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
positions!.value = shrinkOffset;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
25
lib/components/my_toast.dart
Normal file
@ -0,0 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shirne_dialog/shirne_dialog.dart';
|
||||
|
||||
class MyToast {
|
||||
///
|
||||
void tip({
|
||||
required String title,
|
||||
String? type, // 默认失败
|
||||
String? position, // 默认底部显示
|
||||
}) {
|
||||
final baseStyle = position == 'top'
|
||||
? MyDialog.theme.toastStyle?.top()
|
||||
: position == 'center'
|
||||
? MyDialog.theme.toastStyle?.center()
|
||||
: MyDialog.theme.toastStyle?.bottom();
|
||||
MyDialog.toast(
|
||||
title,
|
||||
icon: type == 'success' ? const Icon(Icons.check_circle) : Icon(Icons.warning),
|
||||
duration: Duration(milliseconds: 5000),
|
||||
style: baseStyle?.copyWith(
|
||||
backgroundColor: type == 'success' ? Colors.green.withAlpha(200) : Colors.red.withAlpha(200),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
47
lib/components/network_or_asset_image.dart
Normal file
@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NetworkOrAssetImage extends StatelessWidget {
|
||||
final String? imageUrl;
|
||||
final double width;
|
||||
final double? height;
|
||||
final BoxFit fit;
|
||||
final String placeholderAsset;
|
||||
|
||||
const NetworkOrAssetImage({
|
||||
super.key,
|
||||
required this.imageUrl,
|
||||
this.width = 60.0,
|
||||
this.height,
|
||||
this.fit = BoxFit.cover,
|
||||
this.placeholderAsset = 'assets/images/avatar/default.png',
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isNetwork = imageUrl != null && imageUrl!.isNotEmpty && (imageUrl!.startsWith('http://') || imageUrl!.startsWith('https://'));
|
||||
|
||||
if (isNetwork) {
|
||||
return Image.network(
|
||||
imageUrl!,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Image.asset(
|
||||
placeholderAsset,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return Image.asset(
|
||||
(imageUrl != null && imageUrl!.isNotEmpty) ? imageUrl! : placeholderAsset,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
76
lib/components/preview_video.dart
Normal file
@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:loopin/components/shark_video.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
|
||||
class PreviewVideo extends StatefulWidget {
|
||||
final String videoUrl;
|
||||
final double? width;
|
||||
final double? height;
|
||||
|
||||
const PreviewVideo({
|
||||
super.key,
|
||||
required this.videoUrl,
|
||||
this.width,
|
||||
this.height,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PreviewVideo> createState() => _PreviewVideoPageState();
|
||||
}
|
||||
|
||||
class _PreviewVideoPageState extends State<PreviewVideo> {
|
||||
late final Player _player = Player();
|
||||
late VideoController videoController = VideoController(_player);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_player.open(Media(widget.videoUrl));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_player.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final videoWidth = widget.width ?? 1.0;
|
||||
final videoHeight = widget.height ?? 1.0;
|
||||
final isHorizontal = videoWidth > videoHeight;
|
||||
|
||||
return SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Center(
|
||||
child: Video(
|
||||
controller: videoController,
|
||||
fit: isHorizontal ? BoxFit.contain : BoxFit.cover,
|
||||
controls: (state) => MyMaterialVideoControls(state),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 关闭按钮
|
||||
Positioned(
|
||||
top: 20,
|
||||
left: 20,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:ai_barcode_scanner/ai_barcode_scanner.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class ScanUtil {
|
||||
static Future<void> openScanner({required void Function(String) onResult}) async {
|
||||
@ -42,6 +43,29 @@ class ScanUtil {
|
||||
width: MediaQuery.of(Get.context!).size.width * 0.8,
|
||||
height: MediaQuery.of(Get.context!).size.height * 0.5,
|
||||
),
|
||||
// 异常处理
|
||||
errorBuilder: (context, error) {
|
||||
String message = "无法启动摄像头,请检查权限";
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error, color: Colors.red, size: 60),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message,
|
||||
style: const TextStyle(fontSize: 18, color: Colors.red),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () async => await openAppSettings(),
|
||||
child: const Text('去开启权限'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
|
2062
lib/components/shark_video.dart
Normal file
145
lib/controller/shop_index_controller.dart
Normal file
@ -0,0 +1,145 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:loopin/IM/im_service.dart';
|
||||
import 'package:loopin/api/shop_api.dart';
|
||||
import 'package:loopin/service/http.dart';
|
||||
|
||||
/// 单个 Tab 的状态
|
||||
class TabState {
|
||||
final ScrollController scrollController;
|
||||
final RxInt currentPage;
|
||||
final RxDouble scrollOffset;
|
||||
final RxList dataList;
|
||||
final RxBool isLoading;
|
||||
final RxBool hasLoaded;
|
||||
|
||||
TabState({
|
||||
required this.scrollController,
|
||||
required this.currentPage,
|
||||
required this.scrollOffset,
|
||||
required this.dataList,
|
||||
required this.isLoading,
|
||||
required this.hasLoaded,
|
||||
});
|
||||
}
|
||||
|
||||
class ShopIndexController extends GetxController with GetSingleTickerProviderStateMixin {
|
||||
TabController? tabController;
|
||||
|
||||
///轮播图数据
|
||||
RxList<dynamic> swiperData = <dynamic>[].obs;
|
||||
|
||||
/// tab 分类列表
|
||||
RxList<dynamic> tabList = <dynamic>[].obs;
|
||||
|
||||
/// 每个 tab 对应的状态
|
||||
final Map<int, TabState> tabs = {};
|
||||
|
||||
/// 当前 tab index
|
||||
RxInt currentTabIndex = 0.obs;
|
||||
|
||||
/// 初始化 Tab 分类
|
||||
void initTabs() async {
|
||||
// 释放旧的 ScrollController
|
||||
tabs.forEach((_, state) => state.scrollController.dispose());
|
||||
tabs.clear();
|
||||
tabList.clear();
|
||||
// 先释放旧 TabController(并移除监听)
|
||||
tabController?.removeListener(_tabListener);
|
||||
tabController?.dispose();
|
||||
|
||||
// 赋值 tab 数据
|
||||
final res = await Http.post(ShopApi.shopCategory, data: {
|
||||
'showStatus': 1,
|
||||
});
|
||||
final data = res['data']['records'] as List<dynamic>;
|
||||
logger.w(data);
|
||||
tabList.addAll(data);
|
||||
|
||||
// 初始化每个 tab 的状态
|
||||
for (int i = 0; i < tabList.length; i++) {
|
||||
final controller = ScrollController();
|
||||
tabs[i] = TabState(
|
||||
scrollController: controller,
|
||||
currentPage: 1.obs,
|
||||
scrollOffset: 0.0.obs,
|
||||
dataList: <dynamic>[].obs,
|
||||
isLoading: false.obs,
|
||||
hasLoaded: false.obs,
|
||||
);
|
||||
}
|
||||
|
||||
// 创建新的 TabController
|
||||
tabController = TabController(length: tabList.length, vsync: this);
|
||||
tabController!.addListener(_tabListener);
|
||||
// 初始化第一个 tab 的数据
|
||||
if (tabList.isNotEmpty) {
|
||||
loadSwiperData();
|
||||
loadData(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Tab 切换监听
|
||||
void _tabListener() {
|
||||
if (!tabController!.indexIsChanging) {
|
||||
currentTabIndex.value = tabController!.index;
|
||||
|
||||
final tab = tabs[currentTabIndex.value];
|
||||
if (tab != null && !tab.hasLoaded.value) {
|
||||
loadData(currentTabIndex.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshData(int index) async {
|
||||
await loadSwiperData();
|
||||
final tab = tabs[index];
|
||||
if (tab == null) return;
|
||||
|
||||
tab.currentPage.value = 1;
|
||||
tab.dataList.clear();
|
||||
tab.isLoading.value = false;
|
||||
tab.hasLoaded.value = false;
|
||||
|
||||
await loadData(index);
|
||||
}
|
||||
|
||||
/// 加载pageview数据
|
||||
Future<void> loadData(int index) async {
|
||||
final tab = tabs[index];
|
||||
if (tab == null || tab.isLoading.value) return;
|
||||
|
||||
tab.isLoading.value = true;
|
||||
final res = await Http.post(ShopApi.shopList, data: {
|
||||
'size': 10,
|
||||
'current': tab.currentPage.value,
|
||||
'categoryId': tabList[index]['id'],
|
||||
});
|
||||
|
||||
final data = res['data']['records'];
|
||||
tab.dataList.addAll(data);
|
||||
logger.w(res);
|
||||
|
||||
tab.currentPage.value += 1;
|
||||
tab.isLoading.value = false;
|
||||
tab.hasLoaded.value = true;
|
||||
}
|
||||
|
||||
/// 加载swiper数据
|
||||
Future<void> loadSwiperData() async {
|
||||
final res = await Http.post(ShopApi.shopSwiperList, data: {
|
||||
'type': 1,
|
||||
});
|
||||
final data = res['data'];
|
||||
logger.w(res);
|
||||
swiperData.assignAll(data);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
tabController?.removeListener(_tabListener);
|
||||
tabController?.dispose();
|
||||
tabs.forEach((_, state) => state.scrollController.dispose());
|
||||
super.onClose();
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:loopin/IM/controller/chat_controller.dart';
|
||||
import 'package:loopin/IM/controller/tab_bar_controller.dart';
|
||||
import 'package:loopin/IM/im_service.dart';
|
||||
import 'package:loopin/models/tab_type.dart';
|
||||
import 'package:loopin/pages/video/module/recommend.dart';
|
||||
import 'package:loopin/update/upgrade_service.dart';
|
||||
@ -38,7 +37,7 @@ class _LayoutState extends State<Layout> {
|
||||
// tabs选项
|
||||
List navItems = [
|
||||
BottomNavigationBarItem(icon: Icon(Icons.play_circle_outline), label: '视频'),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.local_mall), label: '团购'),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.local_mall), label: '易选'),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(
|
||||
Icons.camera_alt_rounded,
|
||||
@ -79,7 +78,7 @@ class _LayoutState extends State<Layout> {
|
||||
super.initState();
|
||||
// 页面初始化后检查版本更新
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
UpgradeService.checkUpgrade(context);
|
||||
UpgradeService.checkUpgrade(this);
|
||||
});
|
||||
}
|
||||
|
||||
@ -209,7 +208,7 @@ class _LayoutState extends State<Layout> {
|
||||
|
||||
// 点击底部导航
|
||||
void onTabTap(int index) {
|
||||
logger.i(index);
|
||||
// logger.i(index);
|
||||
if (index == 0) {
|
||||
if (videoModuleController.videoTabIndex.value == 2) {
|
||||
RecommendModule.playVideo();
|
||||
@ -223,8 +222,11 @@ class _LayoutState extends State<Layout> {
|
||||
}
|
||||
if (index == 3) {
|
||||
// 更新会话列表
|
||||
final ctl = Get.find<ChatController>();
|
||||
if (ctl.chatList.isEmpty) {
|
||||
Get.find<ChatController>().getConversationList();
|
||||
}
|
||||
}
|
||||
if (index == 4) {
|
||||
myPageKey.currentState?.refreshData();
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ void main() async {
|
||||
Get.put(TabBarController());
|
||||
// 注入会话列表
|
||||
Get.put(ChatController());
|
||||
|
||||
// 监听app前后台状态
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
WidgetsBinding.instance.addObserver(LifecycleHandler());
|
||||
@ -52,13 +53,23 @@ void main() async {
|
||||
// 检测登录状态
|
||||
if (Common.isLogin()) {
|
||||
// 初始化Im,并进行Im登录
|
||||
await im_core.ImCore.init(sdkAppId: 1600080789);
|
||||
// 缺少userSig先用固定值ios用1587,安卓用188
|
||||
final res = await im_core.ImCore.init(sdkAppId: 1600080789);
|
||||
|
||||
// 缺少userSig先用固定值ios用9365,安卓用0385
|
||||
try {
|
||||
if (res) {
|
||||
await ImService.instance.login(userID: Storage.read('userId'), userSig: Storage.read('userSig'));
|
||||
// String userId = '1909990634551795712'; //15877777777
|
||||
// String userId = '18832510385';
|
||||
} else {
|
||||
logger.w('初始化未完成');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.w(e.toString());
|
||||
Common.logout();
|
||||
}
|
||||
// String userId = '1940667704585248769'; //13212279365
|
||||
// String userId = '1943510443312078850'; //18832510385
|
||||
// String userSig =
|
||||
// 'eJwtzcsOgjAQBdB-6dqQKThth8QdG*JrIRHjTqGYiagNDzUx-rsVWN5zJ3M-IlvtgqdtRCzCAMRsyFzae8cVDywJiAhUNEeUmlDLcDpry*vJOS5FLBUAGNCGxsa*HTfWOyKGvhq149vftH8DhHLSli9*Jdrk2hqTVgdK2dKxXSfKbYuXpnPSu1zl*0fd18skg2Ihvj*7ADL4';
|
||||
// 'eJwtjcEKgkAURf9l1iFPm*e8EdoYYUWFURAtg5nk5VRiEln0703q8p57Ofcj9qtd8LS1SEQUgBh1mY29NXzmDodaQhwrBRIJI0kq1sPsYcpTVbERSRgDAIEi3Tf2VXFtPUfEyFc9bfj6ZwrH4J1Ig4UL-6LX0ihyS7U5bi-Wzd8LzrK8TFs6TJ1sZwWGxlGas71PxPcHwH4y9Q__';
|
||||
// 'eJwtzLEKwjAUheF3ySwlNzXNbcHFxSIOaqTWUUgsF1FDG2tEfHdj2-F8P5wPO2x00tuWFUwknM2GTcbePV1oYEBMhQSeopxyZ65n58iwAjLOOXKF*VhscNTa6FJKEdOonm5-UxJQpZhN2lET3599Xllbv9ZBH2uHuDfvst5tG6FX0EFYVhpOpZ973z8W7PsDmYwyIw__';
|
||||
// await ImService.instance.login(userID: userId, userSig: userSig);
|
||||
}
|
||||
|
48
lib/models/conversation_type.dart
Normal file
@ -0,0 +1,48 @@
|
||||
/// 枚举定义:所有的会话类型分组,用于一级消息分类
|
||||
enum ConversationType {
|
||||
noFriend, // 陌生人消息
|
||||
system, //系统消息
|
||||
newFoucs, //新的关注
|
||||
interaction, //互动
|
||||
order, //订单类通知消息
|
||||
groupNotify, //群通知
|
||||
}
|
||||
|
||||
extension ConversationTypeExtension on ConversationType {
|
||||
String get name {
|
||||
switch (this) {
|
||||
case ConversationType.noFriend:
|
||||
return 'noFriend';
|
||||
case ConversationType.system:
|
||||
return 'system';
|
||||
case ConversationType.newFoucs:
|
||||
return 'newFoucs';
|
||||
case ConversationType.interaction:
|
||||
return 'interaction';
|
||||
case ConversationType.order:
|
||||
return 'order';
|
||||
case ConversationType.groupNotify:
|
||||
return 'groupNotify';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conversationTypeFromString(String? type) {
|
||||
if (type == null) return null;
|
||||
|
||||
if (type.contains('noFriend')) {
|
||||
return ConversationType.noFriend.name;
|
||||
} else if (type.contains('system')) {
|
||||
return ConversationType.system.name;
|
||||
} else if (type.contains('newFoucs')) {
|
||||
return ConversationType.newFoucs.name;
|
||||
} else if (type.contains('interaction')) {
|
||||
return ConversationType.interaction.name;
|
||||
} else if (type.contains('order')) {
|
||||
return ConversationType.order.name;
|
||||
} else if (type.contains('groupNotify')) {
|
||||
return ConversationType.groupNotify.name;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
|
||||
|
||||
class ConversationViewModel {
|
||||
final V2TimConversation conversation;
|
||||
final String? faceUrl;
|
||||
final String isCustomAdmin;
|
||||
late V2TimConversation conversation;
|
||||
String? faceUrl;
|
||||
String? isCustomAdmin;
|
||||
|
||||
ConversationViewModel({
|
||||
required this.conversation,
|
||||
|
97
lib/models/notify_message.type.dart
Normal file
@ -0,0 +1,97 @@
|
||||
/// 枚举定义:所有通知的二级类型
|
||||
enum NotifyMessageType {
|
||||
newFoucs, //新的关注
|
||||
systemNotify, // 系统->通知
|
||||
systemReport, // 系统->举报下架(视频,视频评论)
|
||||
systemCheck, // 系统->审核结果(复审,驳回 ,通过)
|
||||
systemPush, //系统->推广
|
||||
interactionComment, //互动->评论
|
||||
interactionAt, //互动->视频评论中的@
|
||||
interactionLike, //互动->点赞
|
||||
interactionReply, //互动->评论回复
|
||||
orderRecharge, //订单->充值 online
|
||||
orderPay, //订单->订单交易成功通知 online
|
||||
orderRefund, //订单->退款结果通知
|
||||
groupNotifyCheck, //群通知->进群申请 online
|
||||
groupNotifyAccpet, // 群通知->进群审核审核通过 online
|
||||
groupNotifyFail, // 群通知->进群审核审核拒绝 online
|
||||
groupNotifyLeaveUp, // 群通知->群升级为达人群通知
|
||||
}
|
||||
|
||||
extension NotifyMessageTypeExtension on NotifyMessageType {
|
||||
String get name {
|
||||
switch (this) {
|
||||
case NotifyMessageType.newFoucs:
|
||||
return 'newFoucs';
|
||||
case NotifyMessageType.systemNotify:
|
||||
return 'systemNotify';
|
||||
case NotifyMessageType.systemReport:
|
||||
return 'systemReport';
|
||||
case NotifyMessageType.systemCheck:
|
||||
return 'systemCheck';
|
||||
case NotifyMessageType.systemPush:
|
||||
return 'systemPush';
|
||||
case NotifyMessageType.interactionComment:
|
||||
return 'interactionComment';
|
||||
case NotifyMessageType.interactionAt:
|
||||
return 'interactionAt';
|
||||
case NotifyMessageType.interactionLike:
|
||||
return 'interactionLike';
|
||||
case NotifyMessageType.interactionReply:
|
||||
return 'interactionReply';
|
||||
case NotifyMessageType.orderRecharge:
|
||||
return 'orderRecharge';
|
||||
case NotifyMessageType.orderPay:
|
||||
return 'orderPay';
|
||||
case NotifyMessageType.orderRefund:
|
||||
return 'orderRefund';
|
||||
case NotifyMessageType.groupNotifyCheck:
|
||||
return 'groupNotifyCheck';
|
||||
case NotifyMessageType.groupNotifyAccpet:
|
||||
return 'groupNotifyAccpet';
|
||||
case NotifyMessageType.groupNotifyFail:
|
||||
return 'groupNotifyFail';
|
||||
case NotifyMessageType.groupNotifyLeaveUp:
|
||||
return 'groupNotifyLeaveUp';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyMessageTypeFromString(String? type) {
|
||||
switch (type) {
|
||||
case 'newFoucs':
|
||||
return NotifyMessageType.newFoucs.name;
|
||||
case 'systemNotify':
|
||||
return NotifyMessageType.systemNotify.name;
|
||||
case 'systemReport':
|
||||
return NotifyMessageType.systemReport.name;
|
||||
case 'systemCheck':
|
||||
return NotifyMessageType.systemCheck.name;
|
||||
case 'systemPush':
|
||||
return NotifyMessageType.systemPush.name;
|
||||
case 'interactionComment':
|
||||
return NotifyMessageType.interactionComment.name;
|
||||
case 'interactionAt':
|
||||
return NotifyMessageType.interactionAt.name;
|
||||
case 'interactionLike':
|
||||
return NotifyMessageType.interactionLike.name;
|
||||
case 'interactionReply':
|
||||
return NotifyMessageType.interactionReply.name;
|
||||
case 'orderRecharge':
|
||||
return NotifyMessageType.orderRecharge.name;
|
||||
case 'orderPay':
|
||||
return NotifyMessageType.orderPay.name;
|
||||
case 'orderRefund':
|
||||
return NotifyMessageType.orderRefund.name;
|
||||
case 'groupNotifyCheck':
|
||||
return NotifyMessageType.groupNotifyCheck.name;
|
||||
case 'groupNotifyAccpet':
|
||||
return NotifyMessageType.groupNotifyAccpet.name;
|
||||
case 'groupNotifyFail':
|
||||
return NotifyMessageType.groupNotifyFail.name;
|
||||
case 'groupNotifyLeaveUp':
|
||||
return NotifyMessageType.groupNotifyLeaveUp.name;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
6
lib/models/summary_type.dart
Normal file
@ -0,0 +1,6 @@
|
||||
/// 枚举定义:自定义消息标签类型
|
||||
class SummaryType {
|
||||
static const hongbao = 'hongbao';
|
||||
static const shareVideo = 'shareVideo';
|
||||
static const shareTuangou = 'shareTuangou';
|
||||
}
|
@ -74,10 +74,10 @@ class _LoginState extends State<Login> {
|
||||
// 初始化im_sdk
|
||||
await im_core.ImCore.init(sdkAppId: 1600080789);
|
||||
|
||||
// String userId = '1909990634551795712'; //15877777777
|
||||
// String userId = '18832510385';
|
||||
// String userId = '1940667704585248769'; //13212279365
|
||||
// String userId = '1943510443312078850'; //18832510385
|
||||
// String userSig =
|
||||
// 'eJwtzcsOgjAQBdB-6dqQKThth8QdG*JrIRHjTqGYiagNDzUx-rsVWN5zJ3M-IlvtgqdtRCzCAMRsyFzae8cVDywJiAhUNEeUmlDLcDpry*vJOS5FLBUAGNCGxsa*HTfWOyKGvhq149vftH8DhHLSli9*Jdrk2hqTVgdK2dKxXSfKbYuXpnPSu1zl*0fd18skg2Ihvj*7ADL4';
|
||||
// 'eJwtjcEKgkAURf9l1iFPm*e8EdoYYUWFURAtg5nk5VRiEln0703q8p57Ofcj9qtd8LS1SEQUgBh1mY29NXzmDodaQhwrBRIJI0kq1sPsYcpTVbERSRgDAIEi3Tf2VXFtPUfEyFc9bfj6ZwrH4J1Ig4UL-6LX0ihyS7U5bi-Wzd8LzrK8TFs6TJ1sZwWGxlGas71PxPcHwH4y9Q__';
|
||||
// 'eJwtzLEKwjAUheF3ySwlNzXNbcHFxSIOaqTWUUgsF1FDG2tEfHdj2-F8P5wPO2x00tuWFUwknM2GTcbePV1oYEBMhQSeopxyZ65n58iwAjLOOXKF*VhscNTa6FJKEdOonm5-UxJQpZhN2lET3599Xllbv9ZBH2uHuDfvst5tG6FX0EFYVhpOpZ973z8W7PsDmYwyIw__';
|
||||
|
||||
try {
|
||||
@ -87,13 +87,17 @@ class _LoginState extends State<Login> {
|
||||
if (loginRes.success) {
|
||||
// 存储登录信息
|
||||
Storage.write('hasLogged', true);
|
||||
// Storage.write('userSig', userSig);
|
||||
Storage.write('userSig', userSig);
|
||||
Storage.write('userId', userId);
|
||||
// Storage.write('token', obj['access_token']);
|
||||
Storage.write('token', obj['access_token']);
|
||||
// 获取用户账户信息
|
||||
final accountRes = await Http.get('${CommonApi.accountInfo}/$userId');
|
||||
logger.i(accountRes);
|
||||
// 刷新短视频列表
|
||||
final videoController = Get.find<VideoModuleController>();
|
||||
videoController.markNeedRefresh();
|
||||
dialogController.close();
|
||||
|
||||
Get.back();
|
||||
}
|
||||
} catch (e) {
|
||||
@ -126,7 +130,7 @@ class _LoginState extends State<Login> {
|
||||
vcodeText = '获取验证码(${time--})';
|
||||
} else {
|
||||
vcodeText = '获取验证码';
|
||||
time = 6;
|
||||
time = 60;
|
||||
disabled = false;
|
||||
timer.cancel();
|
||||
}
|
||||
|
1813
lib/pages/chat/chat_group.dart
Normal file
2052
lib/pages/chat/chat_no_friend.dart
Normal file
@ -2,16 +2,37 @@
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:shirne_dialog/shirne_dialog.dart';
|
||||
|
||||
class RedPacket extends StatefulWidget {
|
||||
const RedPacket({super.key});
|
||||
final bool flag; // true=群,false=单
|
||||
final void Function(Map<String, dynamic>)? onSend;
|
||||
final int? maxNum; // 红包最大数量
|
||||
const RedPacket({super.key, required this.flag, this.onSend, this.maxNum});
|
||||
|
||||
@override
|
||||
State<RedPacket> createState() => _RedPacketState();
|
||||
}
|
||||
|
||||
class _RedPacketState extends State<RedPacket> {
|
||||
final TextEditingController _amountController = TextEditingController();
|
||||
final TextEditingController _maxNumController = TextEditingController();
|
||||
final TextEditingController _remarkController = TextEditingController();
|
||||
|
||||
String amount = '0.00';
|
||||
String remark = '恭喜发财,大吉大利';
|
||||
// 限制只能输入数字和小数点,且最多两位小数
|
||||
final List<TextInputFormatter> _decimalInputFormatters = [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}')),
|
||||
];
|
||||
@override
|
||||
void dispose() {
|
||||
_amountController.dispose();
|
||||
_maxNumController.dispose();
|
||||
_remarkController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -23,26 +44,32 @@ class _RedPacketState extends State<RedPacket> {
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.only(bottom: 50.0),
|
||||
children: [
|
||||
const SizedBox(height: 10.0),
|
||||
if (widget.flag) const SizedBox(height: 10.0),
|
||||
if (widget.flag)
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white, borderRadius: BorderRadius.circular(10.0),
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Text('红包个数'),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
maxLength: widget.maxNum,
|
||||
controller: _maxNumController,
|
||||
textAlign: TextAlign.right,
|
||||
buildCounter: (_, {required currentLength, maxLength, required isFocused}) => null, // 隐藏计数器
|
||||
decoration: const InputDecoration(
|
||||
hintText: "填写个数",
|
||||
isDense: true,
|
||||
hintStyle: TextStyle(fontSize: 14.0),
|
||||
border: OutlineInputBorder(borderSide: BorderSide.none)
|
||||
),
|
||||
onChanged: (value) {},
|
||||
hintText: "填写个数", isDense: true, hintStyle: TextStyle(fontSize: 14.0), border: OutlineInputBorder(borderSide: BorderSide.none)),
|
||||
onChanged: (value) {
|
||||
// 输入的红包个数
|
||||
setState(() {
|
||||
remark = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const Text('个'),
|
||||
@ -54,24 +81,30 @@ class _RedPacketState extends State<RedPacket> {
|
||||
margin: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white, borderRadius: BorderRadius.circular(10.0),
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Text('总金额'),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _amountController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
textAlign: TextAlign.right,
|
||||
inputFormatters: _decimalInputFormatters,
|
||||
maxLength: 6,
|
||||
buildCounter: (_, {required currentLength, maxLength, required isFocused}) => null, // 隐藏计数器
|
||||
decoration: const InputDecoration(
|
||||
hintText: "¥0.00",
|
||||
isDense: true,
|
||||
hintStyle: TextStyle(fontSize: 14.0),
|
||||
border: OutlineInputBorder(borderSide: BorderSide.none)
|
||||
),
|
||||
hintText: "¥0.00", isDense: true, hintStyle: TextStyle(fontSize: 14.0), border: OutlineInputBorder(borderSide: BorderSide.none)),
|
||||
onChanged: (value) {
|
||||
double val = double.tryParse(value) ?? 0.0;
|
||||
if (val > 200) {
|
||||
_amountController.text = '200';
|
||||
val = 200;
|
||||
}
|
||||
setState(() {
|
||||
amount = value != '' ? value : '0.00';
|
||||
amount = val.toStringAsFixed(2);
|
||||
});
|
||||
},
|
||||
),
|
||||
@ -85,7 +118,8 @@ class _RedPacketState extends State<RedPacket> {
|
||||
margin: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white, borderRadius: BorderRadius.circular(10.0),
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
@ -93,15 +127,26 @@ class _RedPacketState extends State<RedPacket> {
|
||||
Expanded(
|
||||
child: TextField(
|
||||
maxLines: null,
|
||||
maxLength: 16,
|
||||
controller: _remarkController,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textAlign: TextAlign.right,
|
||||
buildCounter: (_, {required currentLength, maxLength, required isFocused}) => null, // 隐藏计数器
|
||||
decoration: const InputDecoration(
|
||||
hintText: "恭喜发财,大吉大利",
|
||||
isDense: true,
|
||||
hintStyle: TextStyle(fontSize: 14.0),
|
||||
border: OutlineInputBorder(borderSide: BorderSide.none)
|
||||
),
|
||||
onChanged: (value) {},
|
||||
border: OutlineInputBorder(borderSide: BorderSide.none)),
|
||||
onChanged: (value) {
|
||||
// 留言内容
|
||||
setState(() {
|
||||
if (value.isEmpty) {
|
||||
remark = '恭喜发财,大吉大利';
|
||||
} else {
|
||||
remark = value;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -110,11 +155,10 @@ class _RedPacketState extends State<RedPacket> {
|
||||
const SizedBox(height: 30.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Text('¥', style: TextStyle(fontSize: 24.0)), Text(amount, style: const TextStyle(fontSize: 36.0))
|
||||
]
|
||||
children: <Widget>[const Text('¥', style: TextStyle(fontSize: 24.0)), Text(amount, style: const TextStyle(fontSize: 36.0))]),
|
||||
const SizedBox(
|
||||
height: 20.0,
|
||||
),
|
||||
const SizedBox(height: 20.0,),
|
||||
UnconstrainedBox(
|
||||
constrainedAxis: Axis.vertical,
|
||||
child: FilledButton(
|
||||
@ -122,18 +166,46 @@ class _RedPacketState extends State<RedPacket> {
|
||||
backgroundColor: WidgetStateProperty.all(Color(0xFFFF7F43)),
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
minimumSize: WidgetStateProperty.all(const Size(180.0, 45.0)),
|
||||
shape: WidgetStatePropertyAll(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0))
|
||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0))),
|
||||
),
|
||||
onPressed: () {
|
||||
double amountDouble = double.tryParse(amount) ?? 0.0;
|
||||
if (amountDouble > 0) {
|
||||
//发送红包
|
||||
widget.onSend!(
|
||||
{
|
||||
'maxNum': widget.maxNum ?? 1,
|
||||
'amount': amount,
|
||||
'remark': remark,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
final baseStyle = MyDialog.theme.toastStyle?.top();
|
||||
MyDialog.toast(
|
||||
'未输入金额',
|
||||
icon: const Icon(Icons.check_circle),
|
||||
duration: Duration(milliseconds: 5000),
|
||||
style: baseStyle?.copyWith(
|
||||
backgroundColor: Colors.red.withAlpha(200),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text(
|
||||
'塞钱进红包',
|
||||
style: TextStyle(fontSize: 16.0),
|
||||
),
|
||||
),
|
||||
onPressed: () {},
|
||||
child: const Text('塞钱进红包', style: TextStyle(fontSize: 16.0),),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10.0,
|
||||
),
|
||||
const SizedBox(height: 10.0,),
|
||||
const Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text('未领取的红包,将于24小时后发起退款', style: TextStyle(color: Colors.grey, fontSize: 12.0),),
|
||||
child: Text(
|
||||
'未领取的红包,将于24小时后发起退款',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -8,6 +8,7 @@ import 'package:loopin/IM/controller/chat_controller.dart';
|
||||
import 'package:loopin/IM/global_badge.dart';
|
||||
import 'package:loopin/IM/im_service.dart';
|
||||
import 'package:loopin/components/scan_util.dart';
|
||||
import 'package:loopin/models/conversation_view_model.dart';
|
||||
import 'package:loopin/utils/parse_message_summary.dart';
|
||||
import 'package:shirne_dialog/shirne_dialog.dart';
|
||||
|
||||
@ -30,6 +31,14 @@ class ChatPageState extends State<ChatPage> {
|
||||
controller = Get.find<ChatController>();
|
||||
}
|
||||
|
||||
void deletConv(context, ConversationViewModel item) async {
|
||||
final res = await ImService.instance.deleteConversation(conversationID: item.conversation.conversationID);
|
||||
if (res.success) {
|
||||
Navigator.of(context).pop();
|
||||
controller.chatList.remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
// 长按坐标点
|
||||
double posDX = 0.0;
|
||||
double posDY = 0.0;
|
||||
@ -41,7 +50,7 @@ class ChatPageState extends State<ChatPage> {
|
||||
}
|
||||
|
||||
// 长按菜单
|
||||
void showContextMenu(BuildContext context) {
|
||||
void showContextMenu(BuildContext context, ConversationViewModel item) {
|
||||
bool isLeft = posDX > MediaQuery.of(context).size.width / 2 ? false : true;
|
||||
bool isTop = posDY > MediaQuery.of(context).size.height / 2 ? false : true;
|
||||
|
||||
@ -92,7 +101,9 @@ class ChatPageState extends State<ChatPage> {
|
||||
style: TextStyle(color: Colors.black87, fontSize: 14.0),
|
||||
),
|
||||
dense: true,
|
||||
onTap: () {},
|
||||
onTap: () {
|
||||
deletConv(context, item);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
@ -306,6 +317,7 @@ class ChatPageState extends State<ChatPage> {
|
||||
onRefresh: handleRefresh,
|
||||
child: Obx(() {
|
||||
final chatList = controller.chatList;
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: BouncingScrollPhysics(),
|
||||
@ -322,11 +334,35 @@ class ChatPageState extends State<ChatPage> {
|
||||
children: <Widget>[
|
||||
// 头图
|
||||
ClipOval(
|
||||
child: Image.network(
|
||||
chatList[index].faceUrl ?? 'https://wuzhongjie.com.cn/download/logo.png',
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final faceUrl = chatList[index].faceUrl;
|
||||
final isNetwork =
|
||||
faceUrl != null && faceUrl.isNotEmpty && (faceUrl.startsWith('http://') || faceUrl.startsWith('https://'));
|
||||
if (isNetwork) {
|
||||
return Image.network(
|
||||
faceUrl,
|
||||
height: 50.0,
|
||||
width: 50.0,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Image.asset(
|
||||
'assets/images/pic1.jpg',
|
||||
height: 50.0,
|
||||
width: 50.0,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return Image.asset(
|
||||
(faceUrl != null && faceUrl.isNotEmpty) ? faceUrl : 'assets/images/pic1.jpg',
|
||||
height: 50.0,
|
||||
width: 50.0,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@ -335,9 +371,17 @@ class ChatPageState extends State<ChatPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// Text(
|
||||
// chatList[index].conversation.showName ?? '',
|
||||
// style: const TextStyle(fontSize: 16.0),
|
||||
// ),
|
||||
Text(
|
||||
chatList[index].conversation.showName ?? '',
|
||||
style: const TextStyle(fontSize: 16.0),
|
||||
style: TextStyle(
|
||||
fontSize: (chatList[index].conversation.conversationGroupList?.isNotEmpty ?? false) ? 20 : 16,
|
||||
fontWeight:
|
||||
(chatList[index].conversation.conversationGroupList?.isNotEmpty ?? false) ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2.0),
|
||||
Text(
|
||||
@ -351,6 +395,7 @@ class ChatPageState extends State<ChatPage> {
|
||||
),
|
||||
),
|
||||
// 右侧
|
||||
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
@ -396,7 +441,7 @@ class ChatPageState extends State<ChatPage> {
|
||||
posDY = details.globalPosition.dy;
|
||||
},
|
||||
onLongPress: () {
|
||||
showContextMenu(context);
|
||||
showContextMenu(context, chatList[index]);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -1,9 +1,24 @@
|
||||
/// 商品详情页
|
||||
library;
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:card_swiper/card_swiper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:loopin/IM/controller/chat_controller.dart';
|
||||
import 'package:loopin/IM/im_message.dart';
|
||||
import 'package:loopin/IM/im_service.dart';
|
||||
import 'package:loopin/api/shop_api.dart';
|
||||
import 'package:loopin/components/my_toast.dart';
|
||||
import 'package:loopin/components/network_or_asset_image.dart';
|
||||
import 'package:loopin/models/summary_type.dart';
|
||||
import 'package:loopin/service/http.dart';
|
||||
import 'package:loopin/utils/index.dart';
|
||||
import 'package:loopin/utils/wxsdk.dart';
|
||||
import 'package:shirne_dialog/shirne_dialog.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
|
||||
|
||||
import '../../behavior/custom_scroll_behavior.dart';
|
||||
import '../../components/backtop.dart';
|
||||
@ -16,18 +31,29 @@ class Goods extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _GoodsState extends State<Goods> {
|
||||
// late int shopId; //商品id
|
||||
dynamic shopObj;
|
||||
late ScrollController scrollController = ScrollController();
|
||||
final ChatController chatController = Get.find<ChatController>();
|
||||
// 滚动位置
|
||||
double scrollOffset = 0;
|
||||
// 分享列表
|
||||
List shareList = [
|
||||
{'icon': 'assets/images/share-wx.png', 'label': '好友'},
|
||||
{'icon': 'assets/images/share-wx.png', 'label': '微信'},
|
||||
{'icon': 'assets/images/share-pyq.png', 'label': '朋友圈'},
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final shopId = Get.arguments;
|
||||
scrollController.addListener(() {
|
||||
setState(() {
|
||||
scrollOffset = scrollController.offset;
|
||||
});
|
||||
});
|
||||
shopDetail(shopId);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -36,8 +62,199 @@ class _GoodsState extends State<Goods> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
///商品详情
|
||||
void shopDetail(shopId) async {
|
||||
try {
|
||||
final res = await Http.get('${ShopApi.shopDetail}/$shopId');
|
||||
logger.e(res['data']);
|
||||
setState(() {
|
||||
shopObj = res['data']; // 注意取 data 部分
|
||||
});
|
||||
} catch (e) {
|
||||
logger.e(e);
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
|
||||
void handleShareClick(int index) {
|
||||
final description = shopObj['describe']; // 商品描述
|
||||
if (index == 1) {
|
||||
// 好友
|
||||
Wxsdk.shareToFriend(title: '快看看我分享的商品', description: description, webpageUrl: 'https://baidu.com');
|
||||
} else if (index == 2) {
|
||||
// 朋友圈
|
||||
Wxsdk.shareToTimeline(title: '快看看我分享的商品', webpageUrl: 'https://baidu.com');
|
||||
}
|
||||
}
|
||||
|
||||
void handlCoverClick(V2TimConversation conv) async {
|
||||
// 发送VideoMsg,获取当前视频信息
|
||||
final userId = conv.userID;
|
||||
//price,title,url,sell
|
||||
final makeJson = jsonEncode({
|
||||
"price": shopObj['price'],
|
||||
"title": shopObj['describe'],
|
||||
"url": shopObj['pic'],
|
||||
"sell": Utils().graceNumber(int.parse(shopObj['sales'] ?? '0')),
|
||||
});
|
||||
final res = await IMMessage().createCustomMessage(
|
||||
data: makeJson,
|
||||
);
|
||||
if (res.success) {
|
||||
final sendRes = await IMMessage().sendMessage(msg: res.data!.messageInfo!, toUserID: userId, cloudCustomData: SummaryType.shareTuangou);
|
||||
if (sendRes.success) {
|
||||
MyToast().tip(
|
||||
title: '分享成功',
|
||||
position: 'center',
|
||||
type: 'success',
|
||||
);
|
||||
Get.back();
|
||||
} else {
|
||||
logger.e(res.desc);
|
||||
}
|
||||
} else {
|
||||
logger.e(res.desc);
|
||||
}
|
||||
}
|
||||
|
||||
// 分享弹框
|
||||
void handleShare() {
|
||||
if (chatController.chatList.isNotEmpty) {
|
||||
chatController.getConversationList();
|
||||
}
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(15.0)),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) {
|
||||
return Material(
|
||||
color: Colors.white,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 分享列表
|
||||
SizedBox(
|
||||
height: 110,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: shareList.length,
|
||||
padding: EdgeInsets.symmetric(horizontal: 0, vertical: 20.0),
|
||||
itemBuilder: (context, index) {
|
||||
return GestureDetector(
|
||||
onTap: () => handleShareClick(index),
|
||||
child: Container(
|
||||
width: 64,
|
||||
margin: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset('${shareList[index]['icon']}', width: 48.0),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
'${shareList[index]['label']}',
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// 会话列表
|
||||
Obx(() {
|
||||
// 这里过滤掉有分组的会话
|
||||
final filteredList = chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).toList();
|
||||
if (filteredList.isEmpty) return SizedBox.shrink();
|
||||
return SizedBox(
|
||||
height: 110,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: filteredList.length,
|
||||
padding: EdgeInsets.symmetric(horizontal: 0, vertical: 20.0),
|
||||
itemBuilder: (context, index) {
|
||||
return GestureDetector(
|
||||
// 点击分享
|
||||
onTap: () => handlCoverClick(filteredList[index].conversation),
|
||||
child: Container(
|
||||
width: 64,
|
||||
margin: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Image.asset('${chatController.chatList[index].faceUrl}', width: 48.0),
|
||||
NetworkOrAssetImage(
|
||||
imageUrl: filteredList[index].faceUrl,
|
||||
width: 48.0,
|
||||
height: 48.0,
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
'${filteredList[index].conversation.showName}',
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// 取消按钮
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: InkWell(
|
||||
onTap: () => Get.back(),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
width: double.infinity,
|
||||
height: 50.0,
|
||||
color: Colors.grey[50],
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: Colors.black87),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (shopObj == null) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
String swiperInfo = shopObj['albumPics'] ?? "";
|
||||
List<String> swiperList;
|
||||
if (swiperInfo.isNotEmpty) {
|
||||
swiperList = swiperInfo.split(','); // 商品详情轮播图
|
||||
} else {
|
||||
swiperList = [];
|
||||
}
|
||||
dynamic attr = shopObj['productAttr']; //json数据
|
||||
List<dynamic> attrList = [];
|
||||
if (!Utils.isEmpty(attr)) {
|
||||
attrList = jsonDecode(attr);
|
||||
}
|
||||
logger.e(attrList);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
body: CustomScrollView(
|
||||
@ -63,32 +280,26 @@ class _GoodsState extends State<Goods> {
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.search,
|
||||
size: 20.0,
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.favorite_border,
|
||||
size: 20.0,
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.share,
|
||||
size: 20.0,
|
||||
),
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
// 分享
|
||||
handleShare();
|
||||
},
|
||||
),
|
||||
],
|
||||
// 自定义伸缩区域(轮播图)
|
||||
flexibleSpace: Container(
|
||||
decoration:
|
||||
BoxDecoration(gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFFF5000), Color(0xFFFFAA00)])),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFFFF5000), Color(0xFFFFAA00)],
|
||||
),
|
||||
),
|
||||
child: FlexibleSpaceBar(
|
||||
background: ScrollConfiguration(
|
||||
behavior: CustomScrollBehavior(),
|
||||
@ -99,20 +310,7 @@ class _GoodsState extends State<Goods> {
|
||||
activeColor: Colors.white,
|
||||
)),
|
||||
indicatorLayout: PageIndicatorLayout.SCALE,
|
||||
children: [
|
||||
Image.network(
|
||||
'https://img13.360buyimg.com/n1/jfs/t1/263909/5/4187/123220/676eb220F3e481086/0cee829b1894fc4c.jpg',
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
Image.network(
|
||||
'https://img13.360buyimg.com/n1/jfs/t1/245928/34/24374/150795/673b0a0cFdb8831f9/9235d1ed7654aa44.jpg',
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
Image.network(
|
||||
'https://img30.360buyimg.com/n1/jfs/t1/240005/25/26374/136411/6756e1f9Fb685b2ec/3be83b3e1a08169d.jpg',
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
],
|
||||
children: swiperList.map((sw) => NetworkOrAssetImage(imageUrl: sw)).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -126,7 +324,12 @@ class _GoodsState extends State<Goods> {
|
||||
Container(
|
||||
padding: EdgeInsets.fromLTRB(15.0, 10.0, 15.0, 25.0),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFFF5000), Color(0xFFFFAA00)])),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFFFF5000), Color(0xFFFFAA00)],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 5.0,
|
||||
@ -134,16 +337,17 @@ class _GoodsState extends State<Goods> {
|
||||
Row(
|
||||
spacing: 5.0,
|
||||
children: [
|
||||
Text(
|
||||
'¥3838',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.0,
|
||||
decoration: TextDecoration.lineThrough,
|
||||
decorationColor: Colors.black,
|
||||
decorationThickness: 1.5,
|
||||
),
|
||||
),
|
||||
// 原价
|
||||
// Text(
|
||||
// '¥${shopObj['price']}',
|
||||
// style: TextStyle(
|
||||
// color: Colors.white,
|
||||
// fontSize: 16.0,
|
||||
// decoration: TextDecoration.lineThrough,
|
||||
// decorationColor: Colors.black,
|
||||
// decorationThickness: 1.5,
|
||||
// ),
|
||||
// ),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 3.0),
|
||||
decoration: BoxDecoration(
|
||||
@ -151,21 +355,23 @@ class _GoodsState extends State<Goods> {
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
),
|
||||
child: Text(
|
||||
'现价¥3800',
|
||||
'¥${shopObj['price']}',
|
||||
style: TextStyle(color: Colors.red, fontSize: 12.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'已售1.1w',
|
||||
// '已售${Utils().graceNumber(shopObj['sales'] ?? 0)}',
|
||||
'已售${Utils().graceNumber(int.tryParse(shopObj['sales']?.toString() ?? '0') ?? 0)}',
|
||||
style: TextStyle(color: Colors.white, fontSize: 12.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.fromLTRB(10.0, 10.0, 10.0, 0),
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFFAFAFA),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(15.0)),
|
||||
@ -176,18 +382,45 @@ class _GoodsState extends State<Goods> {
|
||||
// 标题
|
||||
Container(
|
||||
padding: EdgeInsets.all(5.0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text.rich(
|
||||
TextSpan(children: [
|
||||
TextSpan(text: ' 年货节 ', style: TextStyle(fontSize: 11.0, backgroundColor: const Color(0xFFFF5000), color: Colors.white)),
|
||||
TextSpan(
|
||||
text: ' 茅台(MOUTAI)飞天 53度 酱香型白酒 500ml*2 海外版送礼袋年货送礼',
|
||||
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.w700),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFF5000),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
shopObj['productCategoryName'] ?? '未知分类名称',
|
||||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const WidgetSpan(child: SizedBox(width: 4)),
|
||||
TextSpan(
|
||||
text: '${shopObj['describe'] ?? ''}',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 规格
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 10.0),
|
||||
@ -196,55 +429,77 @@ class _GoodsState extends State<Goods> {
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
// child: Column(
|
||||
// spacing: 10.0,
|
||||
// children: [
|
||||
// Row(
|
||||
// spacing: 5.0,
|
||||
// children: [
|
||||
// Icon(
|
||||
// Icons.timer,
|
||||
// size: 16.0,
|
||||
// ),
|
||||
// Expanded(
|
||||
// child: Text(
|
||||
// '本商品请于2025.01.25前进行核销',
|
||||
// style: TextStyle(fontSize: 12.0),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// Row(
|
||||
// spacing: 5.0,
|
||||
// children: [
|
||||
// Icon(
|
||||
// Icons.house_outlined,
|
||||
// size: 16.0,
|
||||
// ),
|
||||
// Expanded(
|
||||
// child: Text(
|
||||
// '营业时间:7x24',
|
||||
// style: TextStyle(fontSize: 12.0),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// Row(
|
||||
// spacing: 5.0,
|
||||
// children: [
|
||||
// Icon(
|
||||
// Icons.location_on,
|
||||
// size: 16.0,
|
||||
// ),
|
||||
// Expanded(
|
||||
// child: Text(
|
||||
// '河北省唐山市玉田县',
|
||||
// style: TextStyle(fontSize: 12.0),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
child: Column(
|
||||
spacing: 10.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: attrList.map<Widget>((attr) {
|
||||
final attrName = attr['name'] ?? '';
|
||||
final options = attr['options'] as List<dynamic>? ?? [];
|
||||
final optionNames = options.map((o) => o['name']).join(' / ');
|
||||
return Row(
|
||||
children: [
|
||||
Row(
|
||||
spacing: 5.0,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.timer,
|
||||
size: 16.0,
|
||||
Text(
|
||||
'$attrName: ',
|
||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'本商品请于2025.01.25前进行核销',
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
optionNames,
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
spacing: 5.0,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.house_outlined,
|
||||
size: 16.0,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'营业时间:7x24',
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
spacing: 5.0,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.location_on,
|
||||
size: 16.0,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'河北省唐山市玉田县',
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
// 详情
|
||||
@ -255,25 +510,9 @@ class _GoodsState extends State<Goods> {
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
child: Column(
|
||||
spacing: 10.0,
|
||||
children: [
|
||||
Text('【飞天茅台】传承悠久,酱香型白酒典范,四大名酒之一。 【爆款直降】纯粮酿造,固态发酵。 【精髓制作】工艺精湛,入口绵、落口甜、饮后余香。'),
|
||||
Image.network(
|
||||
'https://img30.360buyimg.com/n1/jfs/t1/187328/18/54595/115429/6756e1c7F126ab0d4/fe96f6fd5dfe125d.jpg',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
Image.network(
|
||||
'https://img30.360buyimg.com/n1/jfs/t1/240005/25/26374/136411/6756e1f9Fb685b2ec/3be83b3e1a08169d.jpg',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
Image.network(
|
||||
'https://img30.360buyimg.com/n1/jfs/t1/247398/2/28177/97778/6756da95F518f621c/746dc23032c171ca.jpg',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Html(
|
||||
data: shopObj['detailMobileHtml'] ?? '暂无',
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -296,21 +535,36 @@ class _GoodsState extends State<Goods> {
|
||||
child: Row(
|
||||
spacing: 15.0,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.store,
|
||||
color: Color(0xFFFF5000),
|
||||
size: 18.0,
|
||||
),
|
||||
Text(
|
||||
'店铺',
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
Column(
|
||||
// Column(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// Icon(
|
||||
// Icons.store,
|
||||
// color: Color(0xFFFF5000),
|
||||
// size: 18.0,
|
||||
// ),
|
||||
// Text(
|
||||
// '店铺',
|
||||
// style: TextStyle(fontSize: 12.0),
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
// 可以在这里打开聊天、拨打电话等
|
||||
logger.i('联系客服');
|
||||
final res = await ImService.instance.getConversation(conversationID: 'c2c_${shopObj['shoperId']}');
|
||||
V2TimConversation conversation = res.data;
|
||||
logger.i(conversation.toLogString());
|
||||
if (res.success) {
|
||||
// 客服聊天不用检测关注关系
|
||||
conversation.showName = conversation.showName ?? shopObj['storeName'];
|
||||
Get.toNamed('/chat', arguments: conversation);
|
||||
} else {
|
||||
MyDialog.toast(res.desc, icon: const Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
@ -318,28 +572,29 @@ class _GoodsState extends State<Goods> {
|
||||
size: 18.0,
|
||||
),
|
||||
Text(
|
||||
'客服',
|
||||
'联系商家',
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Badge.count(
|
||||
backgroundColor: Color(0xFFFF5000),
|
||||
count: 6,
|
||||
child: Icon(
|
||||
Icons.shopping_cart_outlined,
|
||||
size: 18.0,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'购物车',
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
// Column(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// Badge.count(
|
||||
// backgroundColor: Color(0xFFFF5000),
|
||||
// count: 6,
|
||||
// child: Icon(
|
||||
// Icons.shopping_cart_outlined,
|
||||
// size: 18.0,
|
||||
// ),
|
||||
// ),
|
||||
// Text(
|
||||
// '购物车',
|
||||
// style: TextStyle(fontSize: 12.0),
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -353,19 +608,19 @@ class _GoodsState extends State<Goods> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.0),
|
||||
child: Text(
|
||||
'加入购物车',
|
||||
style: TextStyle(color: Color(0xFFFF5000), fontSize: 14.0),
|
||||
),
|
||||
),
|
||||
// Padding(
|
||||
// padding: EdgeInsets.symmetric(horizontal: 10.0),
|
||||
// child: Text(
|
||||
// '加入购物车',
|
||||
// style: TextStyle(color: Color(0xFFFF5000), fontSize: 14.0),
|
||||
// ),
|
||||
// ),
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
color: Color(0xFFFF5000),
|
||||
child: Text(
|
||||
'领券购买',
|
||||
'立即购买',
|
||||
style: TextStyle(color: Colors.white, fontSize: 14.0),
|
||||
),
|
||||
),
|
||||
|
@ -1,18 +1,15 @@
|
||||
/// 首页模板
|
||||
library;
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:card_swiper/card_swiper.dart';
|
||||
import 'package:easy_refresh/easy_refresh.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:loopin/IM/im_message.dart';
|
||||
import 'package:loopin/components/custom_sticky_header.dart';
|
||||
|
||||
import '../../behavior/custom_scroll_behavior.dart';
|
||||
import '../../components/backtop.dart';
|
||||
import '../../components/loading.dart';
|
||||
import 'package:loopin/components/backtop.dart';
|
||||
import 'package:loopin/components/loading.dart';
|
||||
import 'package:loopin/controller/shop_index_controller.dart';
|
||||
import 'package:loopin/utils/index.dart';
|
||||
|
||||
class IndexPage extends StatefulWidget {
|
||||
const IndexPage({super.key});
|
||||
@ -23,133 +20,25 @@ class IndexPage extends StatefulWidget {
|
||||
|
||||
class _IndexPageState extends State<IndexPage> with SingleTickerProviderStateMixin {
|
||||
// 分类列表
|
||||
List cateList = [
|
||||
{
|
||||
'id': 1,
|
||||
'list': [
|
||||
{
|
||||
'icon': 'order.svg',
|
||||
'label': '我的订单',
|
||||
},
|
||||
{
|
||||
'icon': 'chongzhi.svg',
|
||||
'label': '充值中心',
|
||||
},
|
||||
{'icon': 'qianbao.svg', 'label': '余额'},
|
||||
{'icon': 'comment.svg', 'label': '评价中心'}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
List<String> tabList = ['推荐', '美食', '娱乐', '文旅', '医疗', '房产'];
|
||||
|
||||
// 瀑布流列表
|
||||
List waterfallData = [
|
||||
{
|
||||
'price': 199.00,
|
||||
'title': '韩料界的萨莉亚!',
|
||||
'shop': '萨莉亚专卖店',
|
||||
'image': 'https://qcloud.dpfile.com/pc/1c3egbzM_ICz90dhi6MAiTsazjxWYQcHCd-sbpD1Wqtph2eIJA04NCRvoGqL4_opG45IiB1YIyNuDTtqzVRwesm_qA1Pf8rFcayTY-n-rG8.jpg',
|
||||
'saleNum': '2.1万'
|
||||
},
|
||||
{
|
||||
'price': 1499.90,
|
||||
'title': '茅台(MOUTAI)飞天 53%vol 500ml 贵州茅台酒(带杯)',
|
||||
'shop': '茅台京东自营旗舰店',
|
||||
'image': 'https://img13.360buyimg.com/n1/jfs/t1/97097/12/15694/245806/5e7373e6Ec4d1b0ac/9d8c13728cc2544d.jpg',
|
||||
'saleNum': '1254'
|
||||
},
|
||||
{
|
||||
'price': 18.90,
|
||||
'title': '上海街头苹果糖!一口一个不吱声',
|
||||
'shop': '芝洛洛自营旗舰店',
|
||||
'image': 'https://p0.meituan.net/coverpic/f0eefdfa02619fb09ca53eacd4d97231123115.jpg',
|
||||
'saleNum': '1.2万'
|
||||
},
|
||||
{
|
||||
'price': 59.00,
|
||||
'title': '谁懂,就是这个菜,尝了第一口,立马决定加单了,真正的咸甜永动机啊🍬 去过云南的朋友都知道,当地的乳扇真的很好吃。',
|
||||
'shop': '薄荷牛舌卷旗舰店',
|
||||
'image': 'https://qcloud.dpfile.com/pc/UcW-v6AN1TxVTt9--5Kaw2-t4W55jUhEG_pM5S-w_AQ4IP3z9WxHzwJ9fOthIjEYY0q73sB2DyQcgmKUxZFQtw.jpg',
|
||||
'saleNum': '1639'
|
||||
},
|
||||
{
|
||||
'price': 2499.00,
|
||||
'title': '小米 REDMI K80 国家补贴 第三代骁龙 8 6550mAh大电池 澎湃OS 玄夜黑 12GB+256GB 红米5G至尊手机',
|
||||
'shop': '小米京东自营旗舰店',
|
||||
'image': 'https://img10.360buyimg.com/n1/s450x450_jfs/t1/264409/38/13856/102861/678dcfdaFb723c58f/5b97cf154bbba96c.jpg',
|
||||
'saleNum': '9726'
|
||||
},
|
||||
{
|
||||
'price': 1.00,
|
||||
'title': '圣菲尔伯爵法国红酒Saintfilcount干红葡萄酒珍藏13.5度单瓶送礼红酒 一元试饮',
|
||||
'shop': '小森葡萄酒专营店',
|
||||
'image': 'https://img10.360buyimg.com/n7/jfs/t1/226168/23/3411/118733/65537e5fF2db2d109/7d1d11a8013d6e8f.jpg',
|
||||
'saleNum': '9.9万'
|
||||
},
|
||||
{
|
||||
'price': 42.00,
|
||||
'title': '美的(Midea)LED便携充电小台灯书桌学习阅读灯学生宿舍卧室床头灯学习台灯',
|
||||
'shop': '美的(Midea)旗舰店',
|
||||
'image': 'https://img14.360buyimg.com/mobilecms/s360x360_jfs/t1/226233/4/10194/156936/658e8f88Fcfc9cb40/cea4a48783f11a7a.jpg',
|
||||
'saleNum': '5106'
|
||||
},
|
||||
{
|
||||
'price': 22.90,
|
||||
'title': '蒙都 羊杂500g 加热即食 京东超市肉干肉脯及礼包11.11真便宜',
|
||||
'shop': '蒙都旗舰店',
|
||||
'image': 'https://img10.360buyimg.com/n7/jfs/t1/155306/32/25324/231912/62d22fb8E4ffab855/c6001ee702fb240a.jpg',
|
||||
'saleNum': '1.6万'
|
||||
},
|
||||
{
|
||||
'price': 19.90,
|
||||
'title': '『 江西炒米粉 』本次最佳😋香就一个字话。锅气的香🔥干辣椒的焦香🌶️油的润香🐷蔬菜混合的清香🥬',
|
||||
'shop': '去月球野餐嗎',
|
||||
'image': 'https://qcloud.dpfile.com/pc/pOAOL-DQRBWfkVZIWYVoy0mMQf6_UutNlOpEpGkT_nz3b1n7ZbpikPgtXMhMsjXNY0q73sB2DyQcgmKUxZFQtw.jpg',
|
||||
'saleNum': '3.2万'
|
||||
},
|
||||
{
|
||||
'price': 109.00,
|
||||
'title': '附近新开业的,作为江西人当然要去试试。点了几个家常菜。',
|
||||
'shop': '辣评新开江西菜',
|
||||
'image': 'https://qcloud.dpfile.com/pc/HePD48CFNnS0kMZyf3Q391wxaW_zVgHimctthH__J6UI54HLPUkNt5e3qtP4Nl2G_aW_B6sGElzX-tSmYRvRnQxxxek7cKy7_R0W-KdxWUk.jpg',
|
||||
'saleNum': '8764'
|
||||
},
|
||||
];
|
||||
// 列表
|
||||
List dataList = [];
|
||||
// 是否加载中
|
||||
bool isLoading = false;
|
||||
|
||||
late ScrollController scrollController = ScrollController();
|
||||
late TabController tabController = TabController(initialIndex: 0, length: tabList.length, vsync: this);
|
||||
final PageController pageController = PageController();
|
||||
// 滚动位置
|
||||
double scrollOffset = 0;
|
||||
|
||||
// 加载更多
|
||||
loadMoreData() async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
setState(() {
|
||||
dataList.addAll(waterfallData);
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
Future<void> handleRefresh() async {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
dataList.clear();
|
||||
for (int i = 0; i < waterfallData.length; i++) {
|
||||
dataList.add(waterfallData[i]);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
// List cateList = [
|
||||
// {
|
||||
// 'id': 1,
|
||||
// 'list': [
|
||||
// {
|
||||
// 'icon': 'order.svg',
|
||||
// 'label': '我的订单',
|
||||
// },
|
||||
// {
|
||||
// 'icon': 'chongzhi.svg',
|
||||
// 'label': '充值中心',
|
||||
// },
|
||||
// {'icon': 'qianbao.svg', 'label': '余额'},
|
||||
// {'icon': 'comment.svg', 'label': '评价中心'}
|
||||
// ]
|
||||
// }
|
||||
// ];
|
||||
final ScrollController pageScrollController = ScrollController();
|
||||
final ShopIndexController controller = Get.put(ShopIndexController());
|
||||
|
||||
// 瀑布流卡片
|
||||
Widget cardList(item) {
|
||||
@ -166,7 +55,7 @@ class _IndexPageState extends State<IndexPage> with SingleTickerProviderStateMix
|
||||
]),
|
||||
child: Column(
|
||||
children: [
|
||||
Image.network('${item['image']}'),
|
||||
Image.network('${item['pic']}'),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
|
||||
child: Column(
|
||||
@ -174,7 +63,7 @@ class _IndexPageState extends State<IndexPage> with SingleTickerProviderStateMix
|
||||
spacing: 5.0,
|
||||
children: [
|
||||
Text(
|
||||
'${item['title']}',
|
||||
'${item['name']}',
|
||||
style: TextStyle(fontSize: 14.0, height: 1.2),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@ -193,13 +82,13 @@ class _IndexPageState extends State<IndexPage> with SingleTickerProviderStateMix
|
||||
]),
|
||||
),
|
||||
Text(
|
||||
'已售${item['saleNum']}件',
|
||||
'已售${Utils().graceNumber(int.parse(item['sales'] ?? '0'))}件',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 10.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'${item['shop']}',
|
||||
'${item['storeName']}',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12.0),
|
||||
),
|
||||
],
|
||||
@ -209,7 +98,7 @@ class _IndexPageState extends State<IndexPage> with SingleTickerProviderStateMix
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
Get.toNamed('/goods');
|
||||
Get.toNamed('/goods', arguments: item['id']);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -217,282 +106,194 @@ class _IndexPageState extends State<IndexPage> with SingleTickerProviderStateMix
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
scrollController.addListener(() {
|
||||
setState(() {
|
||||
scrollOffset = scrollController.offset;
|
||||
});
|
||||
if (scrollController.position.pixels == scrollController.position.maxScrollExtent) {
|
||||
debugPrint('[index]滚动到底部');
|
||||
if (!isLoading) {
|
||||
loadMoreData();
|
||||
}
|
||||
}
|
||||
});
|
||||
// 初始化加载
|
||||
handleRefresh();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.dispose();
|
||||
tabController.dispose();
|
||||
pageController.dispose();
|
||||
super.dispose();
|
||||
controller.initTabs();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
final tabIndex = controller.currentTabIndex.value;
|
||||
final currentTab = controller.tabs[tabIndex];
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
body: ScrollConfiguration(
|
||||
behavior: CustomScrollBehavior().copyWith(scrollbars: false),
|
||||
child: CustomScrollView(
|
||||
scrollBehavior: CustomScrollBehavior().copyWith(scrollbars: false),
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Colors.white,
|
||||
pinned: true,
|
||||
expandedHeight: 200.0,
|
||||
titleSpacing: 10.0,
|
||||
// 搜索框(高斯模糊背景)
|
||||
title: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(30.0),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
|
||||
child: Container(
|
||||
height: 45.0,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withAlpha(200),
|
||||
),
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
hintText: "2025百亿补贴",
|
||||
hintStyle: TextStyle(fontSize: 15.0),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: Colors.black38,
|
||||
size: 21.0,
|
||||
),
|
||||
suffixIcon: Container(
|
||||
padding: EdgeInsets.only(right: 15.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 10.0,
|
||||
body: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.keyboard_voice,
|
||||
color: Colors.black45,
|
||||
size: 21.0,
|
||||
// 顶部固定区域(轮播图 + TabBar)
|
||||
_buildTopSection(),
|
||||
|
||||
// 内容区域
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: controller.tabController,
|
||||
children: controller.tabList.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
return _buildTabContent(index);
|
||||
}).toList(),
|
||||
),
|
||||
Icon(
|
||||
Icons.camera_alt_outlined,
|
||||
color: Colors.black45,
|
||||
size: 21.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 0, horizontal: 10.0),
|
||||
border: OutlineInputBorder(borderSide: BorderSide.none, borderRadius: BorderRadius.circular(30.0))),
|
||||
cursorColor: Colors.black,
|
||||
onChanged: (val) {
|
||||
debugPrint(val);
|
||||
},
|
||||
floatingActionButton: currentTab != null
|
||||
? Backtop(
|
||||
controller: currentTab.scrollController,
|
||||
offset: currentTab.scrollOffset.value,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 构建顶部固定区域
|
||||
Widget _buildTopSection() {
|
||||
double screenWidth = MediaQuery.of(context).size.width;
|
||||
int tabCount = controller.tabList.length;
|
||||
// 每个 Tab 的最小宽度
|
||||
double minTabWidth = 80;
|
||||
// 是否可滚动
|
||||
bool isScrollable = tabCount * minTabWidth > screenWidth;
|
||||
return Column(
|
||||
children: [
|
||||
// 轮播图
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 240,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFFFF5000), Color(0xFFfcaec4)],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.shopping_cart_outlined),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
// 自定义伸缩区域(轮播图)
|
||||
flexibleSpace: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFFF5000), Color(0xFFfcaec4)])),
|
||||
child: FlexibleSpaceBar(
|
||||
background: Swiper.children(
|
||||
child: controller.swiperData.length <= 1
|
||||
? (controller.swiperData.isNotEmpty
|
||||
? Image.network(
|
||||
controller.swiperData.first['images'] ?? '',
|
||||
fit: BoxFit.fill,
|
||||
)
|
||||
: const SizedBox.shrink())
|
||||
: Swiper(
|
||||
itemCount: controller.swiperData.length,
|
||||
autoplay: true,
|
||||
loop: true,
|
||||
pagination: SwiperPagination(
|
||||
builder: DotSwiperPaginationBuilder(
|
||||
color: Colors.white70,
|
||||
activeColor: Colors.white,
|
||||
)),
|
||||
indicatorLayout: PageIndicatorLayout.SCALE,
|
||||
children: [
|
||||
Image.network(
|
||||
'https://m.360buyimg.com/babel/jfs/t20271217/224114/35/38178/150060/6760d559Fd654f946/968c156726b6e822.png',
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
Image.network(
|
||||
'https://m.360buyimg.com/babel/jfs/t20280117/88832/5/48468/139826/6789cbcfF4e0b2a3d/9dc54355b6f65c40.jpg',
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
Image.network(
|
||||
'https://m.360buyimg.com/babel/jfs/t20280108/255505/29/10540/137372/677ddbc1F6cdbbed0/bc477fadedef22a8.jpg',
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 分类
|
||||
// SliverToBoxAdapter(
|
||||
// child: Container(
|
||||
// margin: EdgeInsets.all(10.0),
|
||||
// padding: EdgeInsets.symmetric(vertical: 10.0),
|
||||
// height: 90.0,
|
||||
// clipBehavior: Clip.antiAlias,
|
||||
// decoration: BoxDecoration(
|
||||
// color: Colors.white,
|
||||
// borderRadius: BorderRadius.circular(15.0),
|
||||
// ),
|
||||
// child: Column(
|
||||
// children: [
|
||||
// Expanded(
|
||||
// child: PageView.builder(
|
||||
// controller: pageController,
|
||||
// itemCount: cateList.length,
|
||||
// itemBuilder: (context, index) {
|
||||
// final item = cateList[index];
|
||||
// return GridView.builder(
|
||||
// shrinkWrap: true,
|
||||
// padding: EdgeInsets.zero,
|
||||
// physics: NeverScrollableScrollPhysics(),
|
||||
// gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
// crossAxisCount: 4,
|
||||
// ),
|
||||
// itemCount: item['list'].length,
|
||||
// itemBuilder: (BuildContext context, int index) {
|
||||
// final citem = item['list'][index];
|
||||
// // return Column(
|
||||
// // spacing: 3.0,
|
||||
// // children: [
|
||||
// // if (citem['icon'] != null)
|
||||
// // SvgPicture.asset(
|
||||
// // 'assets/images/svg/${citem['icon']}',
|
||||
// // height: 30.0,
|
||||
// // width: 30.0,
|
||||
// // ),
|
||||
// // Text(citem['label']),
|
||||
// // ],
|
||||
// // );
|
||||
// return GestureDetector(
|
||||
// onTap: () {
|
||||
// logger.i('点击了$index');
|
||||
// // 跳转逻辑,用你自己的目标路由替换
|
||||
// Get.toNamed('/order');
|
||||
// },
|
||||
// child: Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// if (citem['icon'] != null)
|
||||
// SvgPicture.asset(
|
||||
// 'assets/images/svg/${citem['icon']}',
|
||||
// height: 30.0,
|
||||
// width: 30.0,
|
||||
// ),
|
||||
// Text(citem['label']),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// CustomPageViewIndicator(
|
||||
// controller: pageController,
|
||||
// count: cateList.length,
|
||||
// color: Color(0xFFCECECE),
|
||||
// activeColor: Color(0xFFFF5000),
|
||||
// ),
|
||||
// ],
|
||||
// )),
|
||||
// ),
|
||||
|
||||
// tabbar列表
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: CustomStickyHeader(
|
||||
child: PreferredSize(
|
||||
preferredSize: Size.fromHeight(45.0),
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
height: 45.0,
|
||||
child: TabBar(
|
||||
controller: tabController,
|
||||
onTap: (index) {
|
||||
logger.i('点击了第 $index 个 tab');
|
||||
itemBuilder: (context, index) {
|
||||
final imageUrl = controller.swiperData[index]['images'] ?? '';
|
||||
return imageUrl.isNotEmpty ? Image.network(imageUrl, fit: BoxFit.fill) : const SizedBox.shrink();
|
||||
},
|
||||
tabs: tabList.map((v) => Tab(text: v)).toList(),
|
||||
isScrollable: false,
|
||||
),
|
||||
),
|
||||
|
||||
// TabBar
|
||||
Container(
|
||||
color: Colors.white,
|
||||
child: TabBar(
|
||||
controller: controller.tabController,
|
||||
tabs: controller.tabList.map((item) {
|
||||
return Tab(
|
||||
child: Text(item['name'], style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
);
|
||||
}).toList(),
|
||||
isScrollable: isScrollable,
|
||||
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||||
unselectedLabelColor: Colors.black87,
|
||||
labelColor: Color(0xFFFF5000),
|
||||
indicatorColor: Color(0xFFFF5000),
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
unselectedLabelStyle: TextStyle(fontSize: 15.0, fontFamily: 'Microsoft YaHei'),
|
||||
labelStyle: TextStyle(fontSize: 15.0, fontFamily: 'Microsoft YaHei', fontWeight: FontWeight.w700),
|
||||
labelColor: Color.fromARGB(255, 236, 108, 49),
|
||||
indicator: const UnderlineTabIndicator(borderSide: BorderSide(color: Color.fromARGB(255, 236, 108, 49), width: 2.0)),
|
||||
unselectedLabelStyle: const TextStyle(fontSize: 16.0, fontFamily: 'Microsoft YaHei'),
|
||||
labelStyle: const TextStyle(fontSize: 18.0, fontFamily: 'Microsoft YaHei', fontWeight: FontWeight.bold),
|
||||
dividerHeight: 0,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.0),
|
||||
labelPadding: EdgeInsets.symmetric(horizontal: 7.5),
|
||||
indicatorPadding: EdgeInsets.symmetric(horizontal: 15.0, vertical: 5.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 瀑布流列表
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
children: [
|
||||
dataList.isEmpty
|
||||
?
|
||||
// 初始loading提示
|
||||
Column(
|
||||
children: [
|
||||
RefreshProgressIndicator(
|
||||
// 构建标签页内容
|
||||
Widget _buildTabContent(int index) {
|
||||
final tabState = controller.tabs[index]!;
|
||||
|
||||
return Obx(() {
|
||||
if (tabState.dataList.isEmpty && tabState.isLoading.value) {
|
||||
return Center(
|
||||
child: RefreshProgressIndicator(
|
||||
backgroundColor: Colors.white,
|
||||
color: Color(0xFFFF5000),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 添加 下拉刷新
|
||||
return EasyRefresh(
|
||||
onRefresh: () async {
|
||||
await controller.refreshData(index);
|
||||
},
|
||||
header: ClassicHeader(
|
||||
dragText: '下拉刷新',
|
||||
armedText: '释放刷新',
|
||||
readyText: '刷新中...',
|
||||
processingText: '刷新完成',
|
||||
messageText: '最后更新于 %T',
|
||||
),
|
||||
child: CustomScrollView(
|
||||
controller: tabState.scrollController,
|
||||
key: PageStorageKey('tab_$index'),
|
||||
physics: const AlwaysScrollableScrollPhysics(), // 确保可下拉
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.fromLTRB(10, 10, 10, 10),
|
||||
sliver: tabState.dataList.isEmpty
|
||||
? SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height - 500, // 给个足够高度让下拉触发
|
||||
child: Center(child: _emptyTip('暂无数据')),
|
||||
),
|
||||
)
|
||||
: MasonryGridView.count(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
: SliverMasonryGrid.count(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 10.0,
|
||||
crossAxisSpacing: 10.0,
|
||||
itemCount: dataList.length + (isLoading ? 1 : 0),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (index < dataList.length) {
|
||||
return cardList(dataList[index]);
|
||||
} else {
|
||||
return SizedBox.shrink();
|
||||
childCount: tabState.dataList.length,
|
||||
itemBuilder: (context, idx) => cardList(tabState.dataList[idx]),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20.0),
|
||||
child: Center(
|
||||
child: tabState.isLoading.value
|
||||
? const Loading(title: 'loading...')
|
||||
: (tabState.dataList.isNotEmpty ? const Text('没有更多数据了') : const SizedBox.shrink()),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
Opacity(opacity: dataList.isNotEmpty && isLoading ? 1 : 0, child: Loading(title: 'loading...')),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 空状态提示
|
||||
Widget _emptyTip(String text) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 50),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset('assets/images/empty.png', width: 100),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 返回顶部
|
||||
floatingActionButton: Backtop(controller: scrollController, offset: scrollOffset),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
497
lib/pages/index/indexcopy.dart
Normal file
@ -0,0 +1,497 @@
|
||||
/// 首页模板
|
||||
library;
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:card_swiper/card_swiper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:loopin/behavior/custom_scroll_behavior.dart';
|
||||
import 'package:loopin/components/backtop.dart';
|
||||
import 'package:loopin/components/custom_sticky_header.dart';
|
||||
import 'package:loopin/components/loading.dart';
|
||||
import 'package:loopin/components/only_down_scroll_physics.dart';
|
||||
import 'package:loopin/controller/shop_index_controller.dart';
|
||||
|
||||
class IndexPage extends StatefulWidget {
|
||||
const IndexPage({super.key});
|
||||
|
||||
@override
|
||||
State<IndexPage> createState() => _IndexPageState();
|
||||
}
|
||||
|
||||
class _IndexPageState extends State<IndexPage> with SingleTickerProviderStateMixin {
|
||||
// 分类列表
|
||||
// List cateList = [
|
||||
// {
|
||||
// 'id': 1,
|
||||
// 'list': [
|
||||
// {
|
||||
// 'icon': 'order.svg',
|
||||
// 'label': '我的订单',
|
||||
// },
|
||||
// {
|
||||
// 'icon': 'chongzhi.svg',
|
||||
// 'label': '充值中心',
|
||||
// },
|
||||
// {'icon': 'qianbao.svg', 'label': '余额'},
|
||||
// {'icon': 'comment.svg', 'label': '评价中心'}
|
||||
// ]
|
||||
// }
|
||||
// ];
|
||||
final ScrollController pageScrollController = ScrollController();
|
||||
final ShopIndexController controller = Get.put(ShopIndexController());
|
||||
|
||||
// 下拉刷新初始化
|
||||
Future<void> handleRefresh() async {}
|
||||
|
||||
///商品详情
|
||||
void shopDetail() async {
|
||||
// final res = await Http.get('${ShopApi.shopDetail}/1938137499482869762');
|
||||
// logger.e(res['data']);
|
||||
}
|
||||
|
||||
// 瀑布流卡片
|
||||
Widget cardList(item) {
|
||||
return GestureDetector(
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(5),
|
||||
offset: Offset(0.0, 1.0),
|
||||
blurRadius: 1.0,
|
||||
spreadRadius: 0.0,
|
||||
),
|
||||
]),
|
||||
child: Column(
|
||||
children: [
|
||||
Image.network('${item['pic']}'),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 5.0,
|
||||
children: [
|
||||
Text(
|
||||
'${item['name']}',
|
||||
style: TextStyle(fontSize: 14.0, height: 1.2),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Row(
|
||||
spacing: 5.0,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(style: TextStyle(color: Colors.red, fontSize: 12.0, fontWeight: FontWeight.w700, fontFamily: 'Arial'), children: [
|
||||
TextSpan(text: '¥'),
|
||||
TextSpan(
|
||||
text: '${item['price']}',
|
||||
style: TextStyle(
|
||||
fontSize: 16.0,
|
||||
)),
|
||||
]),
|
||||
),
|
||||
Text(
|
||||
'已售${item['sales']}件',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 10.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'${item['shop']}',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
Get.toNamed('/goods');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller.initTabs();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
final tabIndex = controller.currentTabIndex.value;
|
||||
final scrollController = controller.tabs[tabIndex]?.scrollController;
|
||||
final pagesView = controller.tabs[tabIndex];
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
|
||||
body: ScrollConfiguration(
|
||||
behavior: CustomScrollBehavior().copyWith(scrollbars: false),
|
||||
child: CustomScrollView(
|
||||
scrollBehavior: CustomScrollBehavior().copyWith(scrollbars: false),
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Colors.white,
|
||||
pinned: true,
|
||||
expandedHeight: 200.0,
|
||||
titleSpacing: 10.0,
|
||||
// 搜索框(高斯模糊背景)
|
||||
// title: ClipRRect(
|
||||
// borderRadius: BorderRadius.circular(30.0),
|
||||
// child: BackdropFilter(
|
||||
// filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
|
||||
// child: Container(
|
||||
// height: 45.0,
|
||||
// decoration: BoxDecoration(
|
||||
// color: Colors.white.withAlpha(200),
|
||||
// ),
|
||||
// child: TextField(
|
||||
// decoration: InputDecoration(
|
||||
// isDense: true,
|
||||
// hintText: "2025百亿补贴",
|
||||
// hintStyle: TextStyle(fontSize: 15.0),
|
||||
// prefixIcon: Icon(
|
||||
// Icons.search,
|
||||
// color: Colors.black38,
|
||||
// size: 21.0,
|
||||
// ),
|
||||
// suffixIcon: Container(
|
||||
// padding: EdgeInsets.only(right: 15.0),
|
||||
// child: Row(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// spacing: 10.0,
|
||||
// children: [
|
||||
// Icon(
|
||||
// Icons.keyboard_voice,
|
||||
// color: Colors.black45,
|
||||
// size: 21.0,
|
||||
// ),
|
||||
// Icon(
|
||||
// Icons.camera_alt_outlined,
|
||||
// color: Colors.black45,
|
||||
// size: 21.0,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// contentPadding: EdgeInsets.symmetric(vertical: 0, horizontal: 10.0),
|
||||
// border: OutlineInputBorder(borderSide: BorderSide.none, borderRadius: BorderRadius.circular(30.0))),
|
||||
// cursorColor: Colors.black,
|
||||
// onChanged: (val) {
|
||||
// debugPrint(val);
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// actions: [
|
||||
// IconButton(
|
||||
// icon: Icon(Icons.shopping_cart_outlined),
|
||||
// onPressed: () {},
|
||||
// ),
|
||||
// ],
|
||||
// 自定义伸缩区域(轮播图)
|
||||
flexibleSpace: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFFF5000), Color(0xFFfcaec4)])),
|
||||
child: FlexibleSpaceBar(
|
||||
background: Swiper.children(
|
||||
pagination: SwiperPagination(
|
||||
builder: DotSwiperPaginationBuilder(
|
||||
color: Colors.white70,
|
||||
activeColor: Colors.white,
|
||||
),
|
||||
),
|
||||
indicatorLayout: PageIndicatorLayout.SCALE,
|
||||
children: [
|
||||
...controller.swiperData.map((item) {
|
||||
final imageUrl = item['images'] ?? '';
|
||||
return imageUrl.isNotEmpty
|
||||
? Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.fill,
|
||||
)
|
||||
: SizedBox.shrink();
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// tabbar列表
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: CustomStickyHeader(
|
||||
child: PreferredSize(
|
||||
preferredSize: Size.fromHeight(45.0),
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
height: 45.0,
|
||||
child: TabBar(
|
||||
controller: controller.tabController,
|
||||
onTap: (index) {
|
||||
print('点击了第 $index 个 tab');
|
||||
},
|
||||
tabs: controller.tabList.map((v) => Tab(text: v['name'])).toList(),
|
||||
isScrollable: true,
|
||||
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||||
unselectedLabelColor: Colors.black87,
|
||||
labelColor: Color(0xFFFF5000),
|
||||
indicatorColor: Color(0xFFFF5000),
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
unselectedLabelStyle: TextStyle(fontSize: 15.0, fontFamily: 'Microsoft YaHei'),
|
||||
labelStyle: TextStyle(fontSize: 15.0, fontFamily: 'Microsoft YaHei', fontWeight: FontWeight.w700),
|
||||
dividerHeight: 0,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.0),
|
||||
labelPadding: EdgeInsets.symmetric(horizontal: 7.5),
|
||||
indicatorPadding: EdgeInsets.symmetric(horizontal: 15.0, vertical: 5.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 瀑布流列表
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
children: [
|
||||
pagesView?.dataList.isEmpty ?? true
|
||||
?
|
||||
// 初始loading提示
|
||||
Column(
|
||||
children: [
|
||||
RefreshProgressIndicator(
|
||||
backgroundColor: Colors.white,
|
||||
color: Color(0xFFFF5000),
|
||||
),
|
||||
],
|
||||
)
|
||||
: MasonryGridView.count(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 10.0,
|
||||
crossAxisSpacing: 10.0,
|
||||
itemCount: ((pagesView?.dataList.length ?? 0) + (pagesView?.isLoading.value == true ? 1 : 0)),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final dataList = pagesView?.dataList ?? [];
|
||||
if (index < dataList.length) {
|
||||
return cardList(dataList[index]);
|
||||
} else {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
Opacity(
|
||||
opacity: ((pagesView?.dataList.isNotEmpty ?? false) && (pagesView?.isLoading.value ?? false)) ? 1 : 0,
|
||||
child: Loading(title: 'loading...'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 返回顶部
|
||||
floatingActionButton: pagesView != null
|
||||
? Backtop(
|
||||
controller: pagesView.scrollController,
|
||||
offset: pagesView.scrollOffset.value,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Obx(() {
|
||||
// final tabIndex = controller.currentTabIndex.value;
|
||||
// final pagesView = controller.tabs[tabIndex];
|
||||
// return Scaffold(
|
||||
// backgroundColor: Colors.grey[50],
|
||||
// body: NestedScrollViewPlus(
|
||||
// // controller: pageScrollController,
|
||||
// overscrollBehavior: OverscrollBehavior.outer,
|
||||
// physics: (pagesView!.dataList.length > 4 && pagesView.currentPage > 1)
|
||||
// ? const OnlyDownScrollPhysics(parent: AlwaysScrollableScrollPhysics())
|
||||
// : const AlwaysScrollableScrollPhysics(),
|
||||
// headerSliverBuilder: (context, innerBoxIsScrolled) {
|
||||
// return [
|
||||
// SliverAppBar(
|
||||
// backgroundColor: Colors.transparent,
|
||||
// foregroundColor: Colors.white,
|
||||
// pinned: true,
|
||||
// stretch: false,
|
||||
// onStretchTrigger: () async {
|
||||
// print('触发 stretch 拉伸');
|
||||
// // 加载刷新逻辑
|
||||
// },
|
||||
// expandedHeight: 180.0,
|
||||
// // collapsedHeight: kToolbarHeight,
|
||||
// collapsedHeight: 180.0,
|
||||
// // 自定义伸缩区域(轮播图)
|
||||
// flexibleSpace: Container(
|
||||
// decoration: BoxDecoration(
|
||||
// gradient: LinearGradient(
|
||||
// begin: Alignment.topLeft,
|
||||
// end: Alignment.bottomRight,
|
||||
// colors: [
|
||||
// Color(0xFFFF5000),
|
||||
// Color(0xFFfcaec4),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// child: FlexibleSpaceBar(
|
||||
// background: Swiper.children(
|
||||
// pagination: SwiperPagination(
|
||||
// builder: DotSwiperPaginationBuilder(
|
||||
// color: Colors.white70,
|
||||
// activeColor: Colors.white,
|
||||
// ),
|
||||
// ),
|
||||
// indicatorLayout: PageIndicatorLayout.SCALE,
|
||||
// children: [
|
||||
// ...controller.swiperData.map((item) {
|
||||
// final imageUrl = item['images'] ?? '';
|
||||
// return imageUrl.isNotEmpty
|
||||
// ? Image.network(
|
||||
// imageUrl,
|
||||
// fit: BoxFit.fill,
|
||||
// )
|
||||
// : SizedBox.shrink();
|
||||
// }),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// // tab吸顶
|
||||
// SliverPersistentHeader(
|
||||
// pinned: true,
|
||||
// delegate: CustomStickyHeader(
|
||||
// child: PreferredSize(
|
||||
// preferredSize: const Size.fromHeight(48.0),
|
||||
// child: Container(
|
||||
// color: Colors.white,
|
||||
// child: TabBar(
|
||||
// controller: controller.tabController,
|
||||
// tabs: controller.tabList.map((item) {
|
||||
// return Tab(
|
||||
// child: Text(item['name'], style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
// );
|
||||
// }).toList(),
|
||||
// isScrollable: false,
|
||||
// overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||||
// unselectedLabelColor: Colors.black87,
|
||||
// labelColor: const Color(0xFFFF5000),
|
||||
// indicator: const UnderlineTabIndicator(borderSide: BorderSide(color: Color(0xFFFF5000), width: 2.0)),
|
||||
// indicatorSize: TabBarIndicatorSize.tab,
|
||||
// unselectedLabelStyle: const TextStyle(fontSize: 16.0, fontFamily: 'Microsoft YaHei'),
|
||||
// labelStyle: const TextStyle(fontSize: 18.0, fontFamily: 'Microsoft YaHei', fontWeight: FontWeight.bold),
|
||||
// dividerHeight: 0,
|
||||
// padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
// labelPadding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ];
|
||||
// },
|
||||
// body: TabBarView(
|
||||
// controller: controller.tabController,
|
||||
// children: controller.tabList.map((tabItem) {
|
||||
// final index = controller.tabList.indexOf(tabItem);
|
||||
// return buildTabContent(index);
|
||||
// }).toList(),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
|
||||
//子view
|
||||
Widget buildTabContent(int index) {
|
||||
final tabState = controller.tabs[index]!;
|
||||
|
||||
return Obx(() {
|
||||
if (tabState.dataList.isEmpty && tabState.isLoading.value) {
|
||||
return const Center(
|
||||
child: RefreshProgressIndicator(
|
||||
backgroundColor: Colors.white,
|
||||
color: Color(0xFFFF5000),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return CustomScrollView(
|
||||
primary: false,
|
||||
controller: tabState.scrollController,
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
sliver: SliverMasonryGrid.count(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 10.0,
|
||||
crossAxisSpacing: 10.0,
|
||||
childCount: tabState.dataList.length,
|
||||
itemBuilder: (context, idx) => cardList(tabState.dataList[idx]),
|
||||
),
|
||||
),
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20.0),
|
||||
child: Center(
|
||||
child: tabState.isLoading.value ? const Loading(title: 'loading...') : const Text('没有更多数据了'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// 空状态提示
|
||||
Widget emptyTip(String text) {
|
||||
return CustomScrollView(
|
||||
physics: const OnlyDownScrollPhysics(),
|
||||
slivers: [
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 50.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset('assets/images/empty.png', width: 100.0),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 13.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
88
lib/pages/my/des.dart
Normal file
@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:loopin/IM/controller/im_user_info_controller.dart';
|
||||
|
||||
class Des extends StatefulWidget {
|
||||
const Des({super.key});
|
||||
|
||||
@override
|
||||
State<Des> createState() => _DesState();
|
||||
}
|
||||
|
||||
class _DesState extends State<Des> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
final userInfoController = Get.find<ImUserInfoController>();
|
||||
|
||||
void _save() async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
final signature = _formKey.currentState?.fields['signature']?.value;
|
||||
final result = await userInfoController.updateSignature(signature);
|
||||
if (result) {
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
title: const Text('修改简介'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _save,
|
||||
child: const Text(
|
||||
'保存',
|
||||
style: TextStyle(color: Colors.red, fontSize: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: FormBuilder(
|
||||
key: _formKey,
|
||||
child: Obx(() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'简介',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FormBuilderTextField(
|
||||
name: 'signature',
|
||||
initialValue: userInfoController.signature.value,
|
||||
maxLines: 6, // 最多显示6行
|
||||
minLines: 3, // 最少显示3行
|
||||
decoration: const InputDecoration(
|
||||
hintText: '请输入内容',
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
),
|
||||
maxLength: 100,
|
||||
validator: FormBuilderValidators.compose([
|
||||
FormBuilderValidators.required(errorText: '内容不能为空'),
|
||||
FormBuilderValidators.maxLength(100, errorText: '内容不能超过100个字符'),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'最长支持100个字符,请文明用语',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
);
|
||||
})),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -3,12 +3,17 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_rx/src/rx_typedefs/rx_typedefs.dart';
|
||||
import 'package:loopin/IM/controller/im_user_info_controller.dart';
|
||||
import 'package:loopin/IM/im_service.dart';
|
||||
import 'package:loopin/api/video_api.dart';
|
||||
import 'package:loopin/components/custom_sticky_header.dart';
|
||||
import 'package:loopin/components/network_or_asset_image.dart';
|
||||
import 'package:loopin/components/only_down_scroll_physics.dart';
|
||||
import 'package:loopin/controller/video_module_controller.dart';
|
||||
import 'package:loopin/service/http.dart';
|
||||
import 'package:nested_scroll_view_plus/nested_scroll_view_plus.dart';
|
||||
import 'package:shirne_dialog/shirne_dialog.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_follow_info.dart';
|
||||
|
||||
import '../../utils/common.dart';
|
||||
|
||||
@ -17,13 +22,26 @@ class PageParams {
|
||||
int pageSize;
|
||||
bool isLoading;
|
||||
bool hasMore;
|
||||
int total;
|
||||
bool isInitLoading;
|
||||
|
||||
PageParams({
|
||||
this.page = 1,
|
||||
this.pageSize = 10,
|
||||
this.isLoading = false,
|
||||
this.hasMore = true,
|
||||
this.total = 0,
|
||||
this.isInitLoading = true,
|
||||
});
|
||||
|
||||
void init() {
|
||||
page = 1;
|
||||
pageSize = 10;
|
||||
isLoading = false;
|
||||
hasMore = true;
|
||||
total = 0;
|
||||
isInitLoading = true;
|
||||
}
|
||||
}
|
||||
|
||||
class MyPage extends StatefulWidget {
|
||||
@ -37,28 +55,38 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
late RxInt currentTabIndex = 0.obs;
|
||||
late RxList items = [].obs;
|
||||
late RxList favoriteItems = [].obs;
|
||||
late RxMap userInfo = {}.obs;
|
||||
RxBool isLogin = Common.isLogin().obs;
|
||||
//用户基本信息
|
||||
// late Rx<V2TimUserFullInfo?> userInfo = Rx<V2TimUserFullInfo?>(null);
|
||||
|
||||
ImUserInfoController? imUserInfoController;
|
||||
|
||||
// 关注,互关,粉丝数量
|
||||
late Rx<V2TimFollowInfo?> followInfo = Rx<V2TimFollowInfo?>(null);
|
||||
RxBool get shouldFixHeader => (currentTabIndex.value == 0 && items.isEmpty) || (currentTabIndex.value == 1 && favoriteItems.isEmpty) ? true.obs : false.obs;
|
||||
|
||||
List tabList = [
|
||||
{'name': "作品", 'badge': 99},
|
||||
{'name': "作品"},
|
||||
{'name': "喜欢"},
|
||||
];
|
||||
|
||||
late PageParams itemsParams;
|
||||
late PageParams favoriteParams;
|
||||
PageParams itemsParams = PageParams();
|
||||
PageParams favoriteParams = PageParams();
|
||||
|
||||
late TabController tabController;
|
||||
late ScrollController scrollController;
|
||||
|
||||
RxDouble positions = 0.0.obs;
|
||||
|
||||
late Callback tabListener;
|
||||
late Callback scrollListener;
|
||||
|
||||
RxBool isPinned = false.obs; // 是否吸顶
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
itemsParams = PageParams();
|
||||
favoriteParams = PageParams();
|
||||
|
||||
initControllers();
|
||||
|
||||
scrollListener = () {
|
||||
@ -77,16 +105,17 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
|
||||
tabListener = () {
|
||||
currentTabIndex.value = tabController.index;
|
||||
scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeIn);
|
||||
if (tabController.index == 0 && items.isEmpty) {
|
||||
loadData(0);
|
||||
scrollInnerList();
|
||||
} else if (tabController.index == 1 && favoriteItems.isEmpty) {
|
||||
loadData(1);
|
||||
scrollInnerList();
|
||||
}
|
||||
};
|
||||
tabController.addListener(tabListener);
|
||||
|
||||
loadData(0);
|
||||
// loadData(0);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -99,87 +128,121 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void loadData([int? tabIndex]) async {
|
||||
// 添加控制子列表滚动的方法
|
||||
void scrollInnerList([double? offset]) async {
|
||||
if (isPinned.value) {
|
||||
// 如果已经吸顶,先给父滚动权限
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
isPinned.value = false;
|
||||
// 直接滚动到指定位置
|
||||
scrollController.jumpTo(positions.value);
|
||||
// 重置滚动位置
|
||||
positions.value = 0.0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadData([int? tabIndex]) async {
|
||||
final index = tabIndex ?? currentTabIndex.value;
|
||||
if (index == 0) {
|
||||
if (itemsParams.isLoading || !itemsParams.hasMore) return;
|
||||
|
||||
itemsParams.isLoading = true;
|
||||
// itemsParams.isInitLoading = true;
|
||||
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
// 模拟生成新数据
|
||||
List<String> newItems = List.generate(
|
||||
itemsParams.pageSize,
|
||||
(i) => '作品 ${(itemsParams.page - 1) * itemsParams.pageSize + i + 1}',
|
||||
);
|
||||
|
||||
// 模拟判断是否还有更多数据
|
||||
if (itemsParams.page >= 2) {
|
||||
try {
|
||||
final res = await Http.post(VideoApi.myPublicList, data: {
|
||||
"userId": imUserInfoController?.userID.value,
|
||||
"yesOrNo": 0,
|
||||
"current": itemsParams.page,
|
||||
"size": itemsParams.pageSize,
|
||||
});
|
||||
final obj = res['data'];
|
||||
final total = obj['total'];
|
||||
final row = obj['rows'];
|
||||
logger.i(res['data']);
|
||||
// 判断是否还有更多数据
|
||||
if (items.length >= total) {
|
||||
itemsParams.hasMore = false;
|
||||
}
|
||||
|
||||
// 添加新数据,触发响应式更新
|
||||
items.addAll(newItems);
|
||||
items.addAll(row);
|
||||
|
||||
// 页码加一
|
||||
itemsParams.page++;
|
||||
|
||||
} finally {
|
||||
itemsParams.isLoading = false;
|
||||
itemsParams.isInitLoading = false;
|
||||
}
|
||||
} else if (index == 1) {
|
||||
// 喜欢列表同理
|
||||
if (favoriteParams.isLoading || !favoriteParams.hasMore) return;
|
||||
|
||||
favoriteParams.isLoading = true;
|
||||
// favoriteParams.isInitLoading = true;
|
||||
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
try {
|
||||
final res = await Http.post(VideoApi.myPublicList, data: {
|
||||
"userId": imUserInfoController?.userID.value,
|
||||
"yesOrNo": 0,
|
||||
"current": itemsParams.page,
|
||||
"size": itemsParams.pageSize,
|
||||
});
|
||||
final obj = res['data'];
|
||||
final total = obj['total'];
|
||||
final row = obj['rows'];
|
||||
|
||||
List<String> newFavorites = List.generate(
|
||||
favoriteParams.pageSize,
|
||||
(i) => '喜欢 ${(favoriteParams.page - 1) * favoriteParams.pageSize + i + 1}',
|
||||
);
|
||||
|
||||
if (favoriteParams.page >= 2) {
|
||||
favoriteParams.hasMore = false;
|
||||
if (favoriteItems.length >= total) {
|
||||
itemsParams.hasMore = false;
|
||||
}
|
||||
|
||||
favoriteItems.addAll(newFavorites);
|
||||
|
||||
favoriteItems.addAll(row);
|
||||
favoriteParams.page++;
|
||||
|
||||
} finally {
|
||||
favoriteParams.isLoading = false;
|
||||
favoriteParams.isInitLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void initControllers() {
|
||||
tabController = TabController(initialIndex: 0, length: tabList.length, vsync: this);
|
||||
scrollController = ScrollController();
|
||||
if (Common.isLogin()) {
|
||||
imUserInfoController = Get.find<ImUserInfoController>();
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化页面数据
|
||||
void refreshData() {
|
||||
void refreshData([int? tabIndex]) async {
|
||||
if (!mounted) {
|
||||
logger.i('未挂载');
|
||||
return;
|
||||
}
|
||||
isLogin.value = Common.isLogin();
|
||||
if (!Common.isLogin()) return;
|
||||
itemsParams = PageParams();
|
||||
favoriteParams = PageParams();
|
||||
currentTabIndex.value = 0;
|
||||
final idx = tabIndex ?? currentTabIndex.value;
|
||||
// 恢复位置
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
scrollInnerList();
|
||||
});
|
||||
items.clear();
|
||||
favoriteItems.clear();
|
||||
scrollController.animateTo(0, duration: const Duration(milliseconds: 100), curve: Curves.easeIn);
|
||||
itemsParams.init();
|
||||
favoriteParams.init();
|
||||
// currentTabIndex.value = 0;
|
||||
selfInfo();
|
||||
loadData();
|
||||
loadData(idx);
|
||||
}
|
||||
|
||||
// 获取当前登录用户基本信息
|
||||
void selfInfo() async {
|
||||
final resIm = await ImService.instance.selfInfo();
|
||||
if (resIm.success) {
|
||||
for (var user in resIm.data ?? []) {
|
||||
logger.i(user.toLogString());
|
||||
}
|
||||
// imUserInfoController = Get.find<ImUserInfoController>();
|
||||
final res = await ImService.instance.getUserFollowInfo(userIDList: [imUserInfoController!.userID.value]);
|
||||
if (res.success) {
|
||||
//这里少个点赞,从服务端获取
|
||||
// followersCount粉丝,多少人关注了我,mutualFollowersCount互关,followingCount我关注了多少人
|
||||
followInfo.value = res.data!.first;
|
||||
logger.i(followInfo.value!.toJson());
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,12 +323,22 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 如果没登录,直接返回一个登录提示页面
|
||||
return Obx(() {
|
||||
if (!isLogin.value) {
|
||||
return SizedBox();
|
||||
} else {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFFAF6F9),
|
||||
body: Obx(() {
|
||||
return NestedScrollViewPlus(
|
||||
controller: scrollController,
|
||||
physics: shouldFixHeader.value ? const OnlyDownScrollPhysics(parent: AlwaysScrollableScrollPhysics()) : const AlwaysScrollableScrollPhysics(),
|
||||
physics: shouldFixHeader.value
|
||||
? OnlyDownScrollPhysics(parent: AlwaysScrollableScrollPhysics())
|
||||
: isPinned.value
|
||||
? NeverScrollableScrollPhysics()
|
||||
: AlwaysScrollableScrollPhysics(),
|
||||
// physics: shouldFixHeader.value ? OnlyDownScrollPhysics(parent: AlwaysScrollableScrollPhysics()) : AlwaysScrollableScrollPhysics(),
|
||||
overscrollBehavior: OverscrollBehavior.outer,
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) {
|
||||
return [
|
||||
@ -281,12 +354,13 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
// 加载刷新逻辑
|
||||
},
|
||||
actions: [
|
||||
_buildIcon('assets/images/svg/service.svg', () {
|
||||
logger.i('点击客服按钮');
|
||||
}),
|
||||
// _buildIcon('assets/images/svg/service.svg', () {
|
||||
// logger.i('点击客服按钮');
|
||||
// }),
|
||||
const SizedBox(width: 8.0),
|
||||
_buildIcon('assets/images/svg/setting.svg', () {
|
||||
logger.i('点击设置按钮');
|
||||
Get.toNamed('/setting');
|
||||
}),
|
||||
const SizedBox(width: 10.0),
|
||||
],
|
||||
@ -297,7 +371,7 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildStatsCard(),
|
||||
Obx(() => _buildStatsCard()),
|
||||
const SizedBox(height: 10.0),
|
||||
_buildOrderCard(context),
|
||||
const SizedBox(height: 10.0),
|
||||
@ -308,6 +382,8 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: CustomStickyHeader(
|
||||
isPinned: isPinned,
|
||||
positions: positions,
|
||||
child: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(48.0),
|
||||
child: Container(
|
||||
@ -348,16 +424,18 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
controller: tabController,
|
||||
children: [
|
||||
// Tab 1:
|
||||
Obx(() => _buildGridTab(0)),
|
||||
_buildGridTab(0),
|
||||
|
||||
// Tab 2:
|
||||
Obx(() => _buildGridTab(1))
|
||||
_buildGridTab(1)
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 空状态提示
|
||||
Widget emptyTip(String text) {
|
||||
@ -390,16 +468,22 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
|
||||
Widget _buildGridTab(int tabIndex) {
|
||||
final listToShow = tabIndex == 0 ? items : favoriteItems;
|
||||
final params = tabIndex == 0 ? itemsParams : favoriteParams;
|
||||
|
||||
PageParams params = tabIndex == 0 ? itemsParams : favoriteParams;
|
||||
if (params.isInitLoading) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (listToShow.isEmpty) {
|
||||
return emptyTip('暂无相关数据');
|
||||
}
|
||||
|
||||
return Obx(() {
|
||||
return CustomScrollView(
|
||||
// physics: !isPinned.value ? NeverScrollableScrollPhysics() : AlwaysScrollableScrollPhysics(),
|
||||
// physics: AlwaysScrollableScrollPhysics(),
|
||||
key: PageStorageKey('myindex_$tabIndex'),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
padding: EdgeInsets.all(10.0),
|
||||
sliver: SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
@ -409,7 +493,7 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(listToShow[index], style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
child: _buildVdCard(listToShow[index]),
|
||||
);
|
||||
},
|
||||
childCount: listToShow.length,
|
||||
@ -418,7 +502,7 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: 10.0,
|
||||
mainAxisSpacing: 10.0,
|
||||
childAspectRatio: 1.0,
|
||||
childAspectRatio: 0.6,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -432,6 +516,87 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildVdCard(item) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
//去视频详情
|
||||
},
|
||||
onLongPress: () {
|
||||
showModalBottomSheet(
|
||||
context: Get.context!,
|
||||
backgroundColor: Colors.black.withOpacity(0.8),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.lock, color: Colors.white),
|
||||
title: const Text('设为私密', style: TextStyle(color: Colors.white)),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: 修改为私密逻辑
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete, color: Colors.redAccent),
|
||||
title: const Text('删除视频', style: TextStyle(color: Colors.redAccent)),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: 删除逻辑
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
color: Colors.grey[900],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
/// 视频缩略图
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Image.network(
|
||||
item['cover'] ?? item['firstFrameImg'],
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
),
|
||||
),
|
||||
|
||||
/// 右下角的点赞数
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.favorite, color: Colors.white, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${item['likeCounts'] ?? 0}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIcon(String assetPath, VoidCallback onTap) {
|
||||
@ -449,7 +614,7 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
final double maxHeight = 180.0;
|
||||
final double minHeight = 100.0;
|
||||
final double minHeight = 120.0;
|
||||
final double currentHeight = constraints.maxHeight;
|
||||
double ratio = (currentHeight - minHeight) / (maxHeight - minHeight);
|
||||
ratio = ratio.clamp(0.0, 1.0);
|
||||
@ -457,7 +622,12 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned.fill(child: Opacity(opacity: 1.0, child: Image.asset('assets/images/pic2.jpg', fit: BoxFit.cover))),
|
||||
Positioned.fill(
|
||||
child: Opacity(
|
||||
opacity: 1.0,
|
||||
child: NetworkOrAssetImage(imageUrl: imUserInfoController?.customInfo['coverBg'], placeholderAsset: 'assets/images/bk.jpg'),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 15.0,
|
||||
bottom: 0,
|
||||
@ -467,7 +637,19 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
ClipOval(child: Image.asset('assets/images/avatar/img11.jpg', height: 60.0, width: 60.0, fit: BoxFit.cover)),
|
||||
ClipOval(
|
||||
child: Obx(() {
|
||||
final faceUrl = imUserInfoController?.faceUrl.value;
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
child: NetworkOrAssetImage(
|
||||
imageUrl: faceUrl,
|
||||
width: 80,
|
||||
height: 80,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(width: 15.0),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@ -478,11 +660,12 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
|
||||
decoration: BoxDecoration(color: Colors.black.withAlpha((0.3 * 255).round()), borderRadius: BorderRadius.circular(20.0)),
|
||||
child: const Text(
|
||||
'新用户2025',
|
||||
child: Obx(
|
||||
() => Text(
|
||||
imUserInfoController!.nickname.value.isNotEmpty == true ? imUserInfoController!.nickname.value : '昵称',
|
||||
style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold, fontFamily: 'Arial', color: Colors.white),
|
||||
),
|
||||
),
|
||||
)),
|
||||
const SizedBox(width: 8.0),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
@ -502,11 +685,11 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
decoration: BoxDecoration(color: Colors.black.withAlpha((0.3 * 255).round()), borderRadius: BorderRadius.circular(20.0)),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
logger.i('点击个人简介');
|
||||
Clipboard.setData(const ClipboardData(text: '1234'));
|
||||
logger.i('点击id');
|
||||
Clipboard.setData(ClipboardData(text: imUserInfoController!.userID.value));
|
||||
MyDialog.toast('ID已复制', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.green.withAlpha(200)));
|
||||
},
|
||||
child: const Text('ID:32938293892839232', style: TextStyle(fontSize: 12.0, color: Colors.white)),
|
||||
child: Text('ID:${imUserInfoController!.userID.value}', style: TextStyle(fontSize: 12.0, color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -535,13 +718,24 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Column(children: const [Text('9999', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), SizedBox(height: 3.0), Text('获赞')]),
|
||||
Column(children: const [Text('25', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), SizedBox(height: 3.0), Text('互关')]),
|
||||
Column(children: const [Text('11', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), SizedBox(height: 3.0), Text('关注')]),
|
||||
Column(children: const [Text('10', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), SizedBox(height: 3.0), Text('粉丝')]),
|
||||
Column(children: [Text('9999', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), SizedBox(height: 3.0), Text('获赞')]),
|
||||
Column(children: [
|
||||
Text('${followInfo.value?.mutualFollowersCount ?? 0}', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 3.0),
|
||||
Text('互关')
|
||||
]),
|
||||
Column(children: [
|
||||
Text('${followInfo.value?.followingCount ?? 0}', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 3.0),
|
||||
Text('关注')
|
||||
]),
|
||||
Column(children: [
|
||||
Text('${followInfo.value?.followersCount ?? 0}', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 3.0),
|
||||
Text('粉丝')
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -583,10 +777,12 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
||||
_buildOrderIcon('assets/images/ico_order.png', '订单', () {
|
||||
Get.toNamed('/order');
|
||||
}),
|
||||
_buildOrderIcon('assets/images/ico_dhx.png', '余额', () {
|
||||
_buildOrderIcon('assets/images/ico_dhx.png', '余额logout', () {
|
||||
showLogoutDialog(context);
|
||||
}),
|
||||
_buildOrderIcon('assets/images/ico_sh.png', '提现', () {}),
|
||||
_buildOrderIcon('assets/images/ico_sh.png', '提现vloger', () {
|
||||
Get.toNamed('/vloger');
|
||||
}),
|
||||
_buildOrderIcon('assets/images/ico_tgm.png', '推广码', () {}),
|
||||
],
|
||||
),
|
||||
|
86
lib/pages/my/nick_name.dart
Normal file
@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:loopin/IM/controller/im_user_info_controller.dart';
|
||||
|
||||
class NickName extends StatefulWidget {
|
||||
const NickName({super.key});
|
||||
|
||||
@override
|
||||
State<NickName> createState() => _NickNameState();
|
||||
}
|
||||
|
||||
class _NickNameState extends State<NickName> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
final userInfoController = Get.find<ImUserInfoController>();
|
||||
|
||||
void _save() async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
final nickname = _formKey.currentState?.fields['nickname']?.value;
|
||||
final result = await userInfoController.updateNickname(nickname);
|
||||
if (result) {
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
title: const Text('修改昵称'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _save,
|
||||
child: const Text(
|
||||
'保存',
|
||||
style: TextStyle(color: Colors.red, fontSize: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: FormBuilder(
|
||||
key: _formKey,
|
||||
child: Obx(() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'我的昵称',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FormBuilderTextField(
|
||||
name: 'nickname',
|
||||
initialValue: userInfoController.nickname.value,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '请输入昵称',
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
),
|
||||
maxLength: 20,
|
||||
validator: FormBuilderValidators.compose([
|
||||
FormBuilderValidators.required(errorText: '昵称不能为空'),
|
||||
FormBuilderValidators.maxLength(20, errorText: '昵称不能超过20个字符'),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'昵称最长支持20个字符,请文明用语',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
);
|
||||
})),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
70
lib/pages/my/setting.dart
Normal file
@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:loopin/styles/index.dart';
|
||||
|
||||
class Setting extends StatelessWidget {
|
||||
const Setting({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Map<String, dynamic>> settings = [
|
||||
{
|
||||
'icon': Icons.person,
|
||||
'title': '账号设置',
|
||||
'onTap': () => Get.toNamed('/userInfo'),
|
||||
},
|
||||
{
|
||||
'icon': Icons.notifications,
|
||||
'title': '通知设置',
|
||||
'onTap': () => Get.toNamed('/notifications'),
|
||||
},
|
||||
{
|
||||
'icon': Icons.lock,
|
||||
'title': '隐私',
|
||||
'onTap': () => Get.toNamed('/privacy'),
|
||||
},
|
||||
{
|
||||
'icon': Icons.info,
|
||||
'title': '关于我们',
|
||||
'onTap': () => Get.toNamed('/about'),
|
||||
},
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('设置')),
|
||||
backgroundColor: Colors.grey[200],
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
children: settings.map((item) {
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: Icon(item['icon'] as IconData, color: FStyle.c999),
|
||||
title: Text(item['title']),
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
color: FStyle.c999,
|
||||
),
|
||||
onTap: item['onTap'],
|
||||
),
|
||||
if (item != settings.last) const Divider(height: 1, indent: 16, endIndent: 16),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
490
lib/pages/my/user_info.dart
Normal file
@ -0,0 +1,490 @@
|
||||
import 'package:bottom_picker/bottom_picker.dart';
|
||||
import 'package:city_pickers/city_pickers.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:loopin/IM/controller/im_user_info_controller.dart';
|
||||
import 'package:loopin/api/common_api.dart';
|
||||
import 'package:loopin/service/http.dart';
|
||||
import 'package:loopin/styles/index.dart';
|
||||
import 'package:loopin/utils/index.dart';
|
||||
import 'package:loopin/utils/wxsdk.dart';
|
||||
import 'package:shirne_dialog/shirne_dialog.dart';
|
||||
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
|
||||
|
||||
class UserInfo extends StatefulWidget {
|
||||
const UserInfo({super.key});
|
||||
|
||||
@override
|
||||
State<UserInfo> createState() => _UserInfoState();
|
||||
}
|
||||
|
||||
class _UserInfoState extends State<UserInfo> {
|
||||
final userInfoController = Get.find<ImUserInfoController>();
|
||||
late List<Map<String, dynamic>> items;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
items = [
|
||||
{
|
||||
'title': '昵称',
|
||||
'value': userInfoController.nickname,
|
||||
'onTap': () => Get.toNamed('/nickName'),
|
||||
},
|
||||
{
|
||||
'title': '简介',
|
||||
'value': userInfoController.signature,
|
||||
'onTap': () => Get.toNamed('/des'),
|
||||
},
|
||||
{
|
||||
'title': '性别',
|
||||
'value': userInfoController.gender,
|
||||
'onTap': () => setGender(context),
|
||||
},
|
||||
{
|
||||
'title': '生日',
|
||||
'value': userInfoController.birthday,
|
||||
'onTap': () => selectDate(context),
|
||||
},
|
||||
{
|
||||
'title': '区域',
|
||||
'value': userInfoController.customInfo,
|
||||
'onTap': () => pickCity(),
|
||||
},
|
||||
{
|
||||
'title': '绑定微信',
|
||||
'value': userInfoController.customInfo,
|
||||
'onTap': () => wechatLogin(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/// 微信授权
|
||||
Future<void> wechatLogin() async {
|
||||
await Wxsdk.login();
|
||||
}
|
||||
|
||||
/// 选性别
|
||||
void setGender(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Container(
|
||||
height: 300,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Center(child: Text('男')),
|
||||
onTap: () {
|
||||
userInfoController.gender.value = 1;
|
||||
userInfoController.updateGender();
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
ListTile(
|
||||
title: Center(child: Text('女')),
|
||||
onTap: () {
|
||||
userInfoController.gender.value = 2;
|
||||
userInfoController.updateGender();
|
||||
Get.back();
|
||||
}),
|
||||
Divider(),
|
||||
ListTile(
|
||||
title: Center(child: Text('保密')),
|
||||
onTap: () {
|
||||
userInfoController.gender.value = 0;
|
||||
userInfoController.updateGender();
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
Container(
|
||||
height: 10,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Center(child: Text('取消')),
|
||||
onTap: () => Get.back(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 选生日
|
||||
void selectDate(BuildContext context) {
|
||||
DateTime selectedDate = DateTime.now(); // 初始值设为当前时间
|
||||
BottomPicker.date(
|
||||
pickerTitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
child: Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// 获取选择结果逻辑
|
||||
final dateStr = "${selectedDate.year}-${selectedDate.month.toString().padLeft(2, '0')}-${selectedDate.day.toString().padLeft(2, '0')}";
|
||||
print(dateStr);
|
||||
DateTime birthdayDate = DateTime(selectedDate.year, selectedDate.month, selectedDate.day);
|
||||
userInfoController.birthday.value = birthdayDate.millisecondsSinceEpoch ~/ 1000; //秒级时间戳
|
||||
userInfoController.updateBirthday();
|
||||
Get.back();
|
||||
},
|
||||
child: Text('确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
displaySubmitButton: false,
|
||||
displayCloseIcon: false,
|
||||
dismissable: true, // 允许点击空白关闭
|
||||
initialDateTime: DateTime.now(),
|
||||
minDateTime: DateTime(1900),
|
||||
maxDateTime: DateTime(
|
||||
DateTime.now().year,
|
||||
DateTime.now().month,
|
||||
DateTime.now().day,
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
),
|
||||
dateOrder: DatePickerDateOrder.ymd,
|
||||
pickerTextStyle: const TextStyle(fontSize: 16, color: Colors.black87),
|
||||
height: 300,
|
||||
onChange: (date) {
|
||||
selectedDate = date as DateTime;
|
||||
},
|
||||
).show(context);
|
||||
}
|
||||
|
||||
///选所在地
|
||||
void pickCity() async {
|
||||
final result = await CityPickers.showCityPicker(
|
||||
context: context,
|
||||
showType: ShowType.pca, // 显示省市区
|
||||
height: 300.0,
|
||||
borderRadius: 16.0,
|
||||
barrierDismissible: true,
|
||||
theme: Theme.of(context).copyWith(
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
),
|
||||
);
|
||||
if (result != null) {
|
||||
final areaName = '${result.provinceName}-${result.cityName}-${result.areaName}';
|
||||
print(result.toString());
|
||||
print('${result.provinceName}-${result.cityName}-${result.areaName}-${result.areaId}');
|
||||
//修改
|
||||
userInfoController.customInfo['area'] = areaName;
|
||||
userInfoController.customInfo['areaCode'] = '${result.areaId}';
|
||||
userInfoController.updateArea();
|
||||
userInfoController.customInfo.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
///选背景
|
||||
void pickCover(BuildContext context) async {
|
||||
final pickedAssets = await AssetPicker.pickAssets(
|
||||
context,
|
||||
pickerConfig: AssetPickerConfig(
|
||||
textDelegate: const AssetPickerTextDelegate(),
|
||||
pathNameBuilder: (AssetPathEntity album) {
|
||||
return Utils.translateAlbumName(album);
|
||||
},
|
||||
maxAssets: 1,
|
||||
requestType: RequestType.image,
|
||||
filterOptions: FilterOptionGroup(
|
||||
imageOption: const FilterOption(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (pickedAssets != null && pickedAssets.isNotEmpty) {
|
||||
final asset = pickedAssets.first;
|
||||
final file = await asset.file; // 获取实际文件
|
||||
if (file != null) {
|
||||
final fileSizeInBytes = await file.length();
|
||||
final sizeInMB = fileSizeInBytes / (1024 * 1024);
|
||||
if (sizeInMB > 100) {
|
||||
MyDialog.toast('图片大小不能超过100MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
||||
} else {
|
||||
print("图片合法,大小:$sizeInMB MB");
|
||||
//走upload(file)上传图片拿到url地址
|
||||
final istance = MyDialog.loading('上传中');
|
||||
final res = await Http.upload(CommonApi.uploadFile, filePath: file.path);
|
||||
userInfoController.customInfo['coverBg'] = res['data']['url'];
|
||||
userInfoController.updateCover();
|
||||
userInfoController.customInfo.refresh();
|
||||
istance.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///选头像
|
||||
void pickFaceUrl(BuildContext context) async {
|
||||
final pickedAssets = await AssetPicker.pickAssets(
|
||||
context,
|
||||
pickerConfig: AssetPickerConfig(
|
||||
textDelegate: const AssetPickerTextDelegate(),
|
||||
pathNameBuilder: (AssetPathEntity album) {
|
||||
return Utils.translateAlbumName(album);
|
||||
},
|
||||
maxAssets: 1,
|
||||
requestType: RequestType.image,
|
||||
filterOptions: FilterOptionGroup(
|
||||
imageOption: const FilterOption(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (pickedAssets != null && pickedAssets.isNotEmpty) {
|
||||
final asset = pickedAssets.first;
|
||||
final file = await asset.file; // 获取实际文件
|
||||
if (file != null) {
|
||||
final fileSizeInBytes = await file.length();
|
||||
final sizeInMB = fileSizeInBytes / (1024 * 1024);
|
||||
if (sizeInMB > 100) {
|
||||
MyDialog.toast('图片大小不能超过100MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
||||
} else {
|
||||
print("图片合法,大小:$sizeInMB MB");
|
||||
//走upload(file)上传图片拿到url地址
|
||||
final istance = MyDialog.loading('上传中');
|
||||
final res = await Http.upload(CommonApi.uploadFile, filePath: file.path);
|
||||
userInfoController.faceUrl.value = res['data']['url'];
|
||||
userInfoController.updateFaceUrl();
|
||||
userInfoController.customInfo.refresh();
|
||||
istance.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: GestureDetector(
|
||||
onTap: () => pickCover(context),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(100),
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
),
|
||||
child: Row(
|
||||
children: const [
|
||||
Icon(Icons.camera_alt, color: Colors.white, size: 16),
|
||||
SizedBox(width: 4),
|
||||
Text('更换封面', style: TextStyle(color: Colors.white, fontSize: 14)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SizedBox.expand(
|
||||
child: Stack(
|
||||
children: [
|
||||
// 封面图
|
||||
SizedBox(
|
||||
height: 240,
|
||||
width: double.infinity,
|
||||
child: Obx(() {
|
||||
final imageUrl = userInfoController.customInfo['coverBg'];
|
||||
return GestureDetector(
|
||||
onTap: () => pickCover(context),
|
||||
child: Image(
|
||||
image: (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : const AssetImage('assets/images/pic2.jpg') as ImageProvider,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
// 白色内容容器
|
||||
Positioned(
|
||||
top: 220,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(30),
|
||||
topRight: Radius.circular(30),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(30),
|
||||
topRight: Radius.circular(30),
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(top: 60, bottom: 40),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 0),
|
||||
const SizedBox(height: 20),
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
children: items.map((item) {
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Text(
|
||||
item['title'],
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 50),
|
||||
Expanded(child: Obx(() {
|
||||
final val = item['value'];
|
||||
if (val is RxString) {
|
||||
return Text(
|
||||
val.value,
|
||||
style: const TextStyle(color: Colors.black),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
);
|
||||
} else if (val is RxInt) {
|
||||
String displayText;
|
||||
if (item['title'] == '性别') {
|
||||
displayText = val.value == 0
|
||||
? '保密'
|
||||
: val.value == 1
|
||||
? '男'
|
||||
: val.value == 2
|
||||
? '女'
|
||||
: '';
|
||||
} else {
|
||||
// 生日
|
||||
// displayText = val.value == 0 ? '' : val.value.toString();
|
||||
if (val.value == 0) {
|
||||
displayText = '';
|
||||
} else {
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(val.value * 1000);
|
||||
displayText = "${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}";
|
||||
}
|
||||
}
|
||||
return Text(
|
||||
displayText,
|
||||
style: const TextStyle(color: Colors.black),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
);
|
||||
} else if (val is String) {
|
||||
return Text(
|
||||
val,
|
||||
style: const TextStyle(color: Colors.black),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
);
|
||||
} else if (val is Map) {
|
||||
if (item['title'] == '区域') {
|
||||
return Text(
|
||||
val['area'] ?? '',
|
||||
style: const TextStyle(color: Colors.black),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
);
|
||||
} else {
|
||||
String wxText = val['unionId'] == null || val['unionId'] == '' ? '未授权' : '已授权';
|
||||
return Row(
|
||||
children: [
|
||||
Spacer(),
|
||||
Text(
|
||||
wxText,
|
||||
style: const TextStyle(color: Colors.black),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}))
|
||||
],
|
||||
),
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
color: FStyle.c999,
|
||||
),
|
||||
onTap: item['onTap'],
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 头像层级最高,放在Stack最后面,覆盖白色容器和封面图
|
||||
Positioned(
|
||||
top: 220 - 60, // 封面图高度减去头像半径,使头像中线对齐封面底线
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Obx(() {
|
||||
final avatar = userInfoController.faceUrl.value;
|
||||
return Center(
|
||||
child: GestureDetector(
|
||||
onTap: () => pickFaceUrl(context),
|
||||
child: CircleAvatar(
|
||||
radius: 60,
|
||||
backgroundColor: Colors.white,
|
||||
child: CircleAvatar(
|
||||
radius: 57,
|
||||
backgroundImage: avatar.isNotEmpty ? NetworkImage(avatar) : const AssetImage('assets/images/avatar/img11.jpg') as ImageProvider,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
648
lib/pages/my/vloger.dart
Normal file
@ -0,0 +1,648 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_rx/src/rx_typedefs/rx_typedefs.dart';
|
||||
import 'package:loopin/IM/im_service.dart';
|
||||
import 'package:loopin/components/custom_sticky_header.dart';
|
||||
import 'package:loopin/components/network_or_asset_image.dart';
|
||||
import 'package:loopin/components/only_down_scroll_physics.dart';
|
||||
import 'package:loopin/styles/index.dart';
|
||||
import 'package:nested_scroll_view_plus/nested_scroll_view_plus.dart';
|
||||
import 'package:shirne_dialog/shirne_dialog.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_follow_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart';
|
||||
|
||||
class PageParams {
|
||||
int page;
|
||||
int pageSize;
|
||||
bool isLoading;
|
||||
bool hasMore;
|
||||
|
||||
PageParams({
|
||||
this.page = 1,
|
||||
this.pageSize = 10,
|
||||
this.isLoading = false,
|
||||
this.hasMore = true,
|
||||
});
|
||||
}
|
||||
|
||||
class Vloger extends StatefulWidget {
|
||||
const Vloger({super.key});
|
||||
|
||||
@override
|
||||
State<Vloger> createState() => MyPageState();
|
||||
}
|
||||
|
||||
class MyPageState extends State<Vloger> with SingleTickerProviderStateMixin {
|
||||
late dynamic args;
|
||||
late RxInt currentTabIndex = 0.obs;
|
||||
late RxList items = [].obs;
|
||||
late RxList favoriteItems = [].obs;
|
||||
late final Rx<V2TimUserFullInfo> userInfo = Rx(V2TimUserFullInfo(
|
||||
userID: '',
|
||||
nickName: '',
|
||||
faceUrl: '',
|
||||
selfSignature: '',
|
||||
gender: 0,
|
||||
customInfo: {
|
||||
'coverBg': '',
|
||||
},
|
||||
role: 0,
|
||||
));
|
||||
|
||||
late RxInt followed = 0.obs; // 是否关注
|
||||
// followersCount粉丝,多少人关注了我,mutualFollowersCount互关,followingCount我关注了多少人
|
||||
late final Rx<V2TimFollowInfo> followInfo = Rx(V2TimFollowInfo(
|
||||
followersCount: 0,
|
||||
followingCount: 0,
|
||||
));
|
||||
|
||||
RxBool get shouldFixHeader => (currentTabIndex.value == 0 && items.isEmpty) || (currentTabIndex.value == 1 && favoriteItems.isEmpty) ? true.obs : false.obs;
|
||||
|
||||
List tabList = [
|
||||
{'name': "作品"},
|
||||
];
|
||||
|
||||
late PageParams itemsParams;
|
||||
late PageParams favoriteParams;
|
||||
|
||||
late TabController tabController;
|
||||
late ScrollController scrollController;
|
||||
|
||||
late Callback tabListener;
|
||||
late Callback scrollListener;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
args = Get.arguments ?? {};
|
||||
itemsParams = PageParams();
|
||||
favoriteParams = PageParams();
|
||||
selfInfo();
|
||||
flowInfo();
|
||||
checkFollowType();
|
||||
initControllers();
|
||||
|
||||
scrollListener = () {
|
||||
final pos = scrollController.position;
|
||||
final isNearBottom = pos.pixels >= pos.maxScrollExtent - 100;
|
||||
|
||||
if (!isNearBottom) return;
|
||||
|
||||
if (currentTabIndex.value == 0 && !itemsParams.isLoading && itemsParams.hasMore) {
|
||||
loadData(0);
|
||||
} else if (currentTabIndex.value == 1 && !favoriteParams.isLoading && favoriteParams.hasMore) {
|
||||
loadData(1);
|
||||
}
|
||||
};
|
||||
scrollController.addListener(scrollListener);
|
||||
|
||||
tabListener = () {
|
||||
currentTabIndex.value = tabController.index;
|
||||
scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeIn);
|
||||
loadData(0);
|
||||
};
|
||||
tabController.addListener(tabListener);
|
||||
|
||||
loadData(0);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
tabController.removeListener(tabListener);
|
||||
scrollController.removeListener(scrollListener);
|
||||
|
||||
tabController.dispose();
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void loadData([int? tabIndex]) async {
|
||||
final index = tabIndex ?? currentTabIndex.value;
|
||||
if (index == 0) {
|
||||
if (itemsParams.isLoading || !itemsParams.hasMore) return;
|
||||
|
||||
itemsParams.isLoading = true;
|
||||
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
// 模拟生成新数据
|
||||
List<String> newItems = List.generate(
|
||||
itemsParams.pageSize,
|
||||
(i) => '作品 ${(itemsParams.page - 1) * itemsParams.pageSize + i + 1}',
|
||||
);
|
||||
|
||||
// 模拟判断是否还有更多数据
|
||||
if (itemsParams.page >= 2) {
|
||||
itemsParams.hasMore = false;
|
||||
}
|
||||
|
||||
// 添加新数据,触发响应式更新
|
||||
items.addAll(newItems);
|
||||
|
||||
// 页码加一
|
||||
itemsParams.page++;
|
||||
|
||||
itemsParams.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
void initControllers() {
|
||||
tabController = TabController(initialIndex: 0, length: tabList.length, vsync: this);
|
||||
scrollController = ScrollController();
|
||||
}
|
||||
|
||||
// 获取当前博主基本信息
|
||||
void selfInfo() async {
|
||||
final resIm = await ImService.instance.otherInfo(args['memberId']);
|
||||
if (resIm.success && resIm.data != null) {
|
||||
userInfo.value = resIm.data!;
|
||||
logger.i(userInfo.value.toLogString());
|
||||
} else {
|
||||
logger.e(resIm.desc);
|
||||
}
|
||||
}
|
||||
|
||||
// 博主的关注与粉丝
|
||||
void flowInfo() async {
|
||||
logger.w(args.toString());
|
||||
final res = await ImService.instance.getUserFollowInfo(userIDList: [args['memberId']]);
|
||||
if (res.success && res.data?.first != null) {
|
||||
//这里少个点赞,从服务端获取
|
||||
// followersCount粉丝,多少人关注了我,mutualFollowersCount互关,followingCount我关注了多少人
|
||||
followInfo.value = res.data!.first;
|
||||
logger.i(followInfo.value.toJson());
|
||||
} else {
|
||||
logger.e(res.desc);
|
||||
}
|
||||
}
|
||||
|
||||
// 检测当前用户是否关注博主
|
||||
void checkFollowType() async {
|
||||
/// 0:不是好友也没有关注
|
||||
/// 1:你关注了对方(单向)
|
||||
/// 2:对方关注了你(单向)
|
||||
/// 3:互相关注(双向好友)
|
||||
final res = await ImService.instance.checkFollowType(userIDList: [args['memberId']]);
|
||||
if (res.success) {
|
||||
final followType = res.data?.first.followType ?? 0;
|
||||
logger.i(res.data?.first.toJson());
|
||||
followed.value = followType;
|
||||
logger.i(followed.value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFFAF6F9),
|
||||
body: Obx(() {
|
||||
return NestedScrollViewPlus(
|
||||
controller: scrollController,
|
||||
physics: shouldFixHeader.value ? const OnlyDownScrollPhysics(parent: AlwaysScrollableScrollPhysics()) : const AlwaysScrollableScrollPhysics(),
|
||||
overscrollBehavior: OverscrollBehavior.outer,
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) {
|
||||
return [
|
||||
SliverAppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
expandedHeight: 180.0,
|
||||
collapsedHeight: 120.0,
|
||||
pinned: true,
|
||||
stretch: true,
|
||||
onStretchTrigger: () async {
|
||||
logger.i('触发 stretch 拉伸');
|
||||
// 加载刷新逻辑
|
||||
},
|
||||
flexibleSpace: Obx(() {
|
||||
userInfo.value;
|
||||
return _buildFlexibleSpace();
|
||||
}),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Obx(() => _buildStatsCard()),
|
||||
const SizedBox(height: 10.0),
|
||||
Obx(() => _buildInfoDesc(context)),
|
||||
const SizedBox(height: 10.0),
|
||||
Obx(() => _buildFoucsButton(context)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: CustomStickyHeader(
|
||||
child: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(48.0),
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
child: TabBar(
|
||||
controller: tabController,
|
||||
tabs: tabList.map((item) {
|
||||
return Tab(
|
||||
child: Badge.count(
|
||||
backgroundColor: Colors.red,
|
||||
count: item['badge'] ?? 0,
|
||||
isLabelVisible: item['badge'] != null,
|
||||
alignment: Alignment.topRight,
|
||||
offset: const Offset(14, -6),
|
||||
child: Text(item['name'], style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
isScrollable: true, //禁止左右滑动
|
||||
tabAlignment: TabAlignment.start,
|
||||
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||||
unselectedLabelColor: Colors.black87,
|
||||
labelColor: Colors.black,
|
||||
indicator: const UnderlineTabIndicator(borderSide: BorderSide(color: Colors.transparent, width: 2.0)),
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
unselectedLabelStyle: const TextStyle(fontSize: 16.0, fontFamily: 'Microsoft YaHei'),
|
||||
labelStyle: const TextStyle(fontSize: 18.0, fontFamily: 'Microsoft YaHei', fontWeight: FontWeight.bold),
|
||||
dividerHeight: 0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
labelPadding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
body: TabBarView(
|
||||
controller: tabController,
|
||||
children: [
|
||||
// Tab 1:
|
||||
Obx(() => _buildGridTab(0)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// 空状态提示
|
||||
Widget emptyTip(String text) {
|
||||
return CustomScrollView(
|
||||
physics: const OnlyDownScrollPhysics(),
|
||||
slivers: [
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 50.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset('assets/images/empty.png', width: 100.0),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 13.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGridTab(int tabIndex) {
|
||||
final listToShow = tabIndex == 0 ? items : favoriteItems;
|
||||
final params = tabIndex == 0 ? itemsParams : favoriteParams;
|
||||
|
||||
if (listToShow.isEmpty) {
|
||||
return emptyTip('暂无相关数据');
|
||||
}
|
||||
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
sliver: SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue[100 * ((index % 8) + 1)],
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(listToShow[index], style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
);
|
||||
},
|
||||
childCount: listToShow.length,
|
||||
),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: 10.0,
|
||||
mainAxisSpacing: 10.0,
|
||||
childAspectRatio: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20.0),
|
||||
child: Center(
|
||||
child: params.hasMore ? const CircularProgressIndicator() : const Text('没有更多数据了'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFlexibleSpace() {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
final double maxHeight = 180.0;
|
||||
final double minHeight = 100.0;
|
||||
final double currentHeight = constraints.maxHeight;
|
||||
double ratio = (currentHeight - minHeight) / (maxHeight - minHeight);
|
||||
ratio = ratio.clamp(0.0, 1.0);
|
||||
String coverBg = userInfo.value.customInfo?['coverBg'] ?? '';
|
||||
coverBg = coverBg.isEmpty ? 'assets/images/pic2.jpg' : coverBg;
|
||||
logger.w(coverBg);
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Opacity(
|
||||
opacity: 1.0,
|
||||
child: Image.asset(
|
||||
coverBg,
|
||||
fit: BoxFit.cover,
|
||||
))),
|
||||
Positioned(
|
||||
left: 15.0,
|
||||
bottom: 0,
|
||||
right: 15.0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
ClipOval(
|
||||
child: NetworkOrAssetImage(imageUrl: userInfo.value.faceUrl),
|
||||
// child: Image.asset(
|
||||
// userInfo.value.faceUrl ?? 'assets/images/pic1.jpg',
|
||||
// height: 60.0,
|
||||
// width: 60.0,
|
||||
// fit: BoxFit.cover,
|
||||
// errorBuilder: (context, error, stackTrace) {
|
||||
// return Image.asset(
|
||||
// 'assets/images/pic1.jpg',
|
||||
// height: 60.0,
|
||||
// width: 60.0,
|
||||
// fit: BoxFit.cover,
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
),
|
||||
const SizedBox(width: 15.0),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
|
||||
decoration: BoxDecoration(color: Colors.black.withAlpha((0.3 * 255).round()), borderRadius: BorderRadius.circular(20.0)),
|
||||
child: Text(
|
||||
userInfo.value.nickName ?? '未知',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 20.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'Arial',
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
|
||||
decoration: BoxDecoration(color: Colors.black.withAlpha((0.3 * 255).round()), borderRadius: BorderRadius.circular(20.0)),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
logger.i('点击个ID');
|
||||
Clipboard.setData(ClipboardData(text: '${userInfo.value.userID}'));
|
||||
MyDialog.toast('ID已复制', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.green.withAlpha(200)));
|
||||
},
|
||||
child: Text('ID:${userInfo.value.userID}', style: TextStyle(fontSize: 12.0, color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatsCard() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withAlpha(10), offset: const Offset(0.0, 1.0), blurRadius: 2.0, spreadRadius: 0.0)],
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Column(children: [Text('9999', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), SizedBox(height: 3.0), Text('获赞')]),
|
||||
Column(children: [
|
||||
Text('${followInfo.value.followingCount}', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 3.0),
|
||||
Text('关注')
|
||||
]),
|
||||
Column(children: [
|
||||
Text('${followInfo.value.followersCount}', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 3.0),
|
||||
Text('粉丝')
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoDesc(BuildContext context) {
|
||||
final tx = userInfo.value.selfSignature;
|
||||
if (tx == null || tx.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withAlpha(10), offset: const Offset(0.0, 1.0), blurRadius: 2.0, spreadRadius: 0.0)],
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'${userInfo.value.selfSignature}',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
/// 关注按钮
|
||||
Widget _buildFoucsButton(BuildContext context) {
|
||||
// final vlogerId = '1943510443312078850'; // 18832510385,后面改回博主的id
|
||||
final vlogerId = args['vlogerId'];
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
transitionBuilder: (child, animation) {
|
||||
final offsetAnimation = Tween<Offset>(
|
||||
begin: Offset((followed.value == 0) ? -1 : 1, 0),
|
||||
end: Offset.zero,
|
||||
).animate(animation);
|
||||
|
||||
return SlideTransition(
|
||||
position: offsetAnimation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: [1, 3].contains(followed.value)
|
||||
? Row(
|
||||
key: const ValueKey('followed'),
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
print('点击已关注');
|
||||
final res = await ImService.instance.unfollowUser(userIDList: [vlogerId]);
|
||||
if (res.success) {
|
||||
// 如果为1那么状态置为0,为3则置为2
|
||||
followed.value = followed.value == 1 ? 0 : 2;
|
||||
// 取关后不需重置陌生人消息group
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey[300],
|
||||
foregroundColor: Colors.black87,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(followed.value == 1
|
||||
? '已关注'
|
||||
: followed.value == 3
|
||||
? '互关'
|
||||
: '未知状态'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
print('私信');
|
||||
// 获取指定会话
|
||||
final res = await ImService.instance.getConversation(conversationID: 'c2c_$vlogerId');
|
||||
V2TimConversation conversation = res.data;
|
||||
logger.i(conversation.toLogString());
|
||||
if (res.success) {
|
||||
// final isFriend = await ImService.instance.isMyFriend(vlogerId, FriendTypeEnum.V2TIM_FRIEND_TYPE_BOTH);
|
||||
// 这里需要注意处理取关后重新关注逻辑
|
||||
// 是否互相关注
|
||||
if (followed.value == 3) {
|
||||
Get.toNamed('/chat', arguments: conversation);
|
||||
} else {
|
||||
logger.i('对方没关注我');
|
||||
logger.i(conversation.toLogString());
|
||||
conversation.showName = conversation.showName ?? userInfo.value.nickName;
|
||||
Get.toNamed('/chatNoFriend', arguments: conversation);
|
||||
}
|
||||
} else {
|
||||
MyDialog.toast(res.desc, icon: const Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey[300],
|
||||
foregroundColor: Colors.black87,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Text('私信'),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: SizedBox(
|
||||
key: const ValueKey('not_followed'),
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
// 0没关系,2对方关注了我
|
||||
print('点击关注');
|
||||
final res = await ImService.instance.followUser(userIDList: [vlogerId]);
|
||||
if (res.success) {
|
||||
followed.value = followed.value == 0 ? 1 : 3;
|
||||
if (followed.value == 3) {
|
||||
// 修改后若为3,我将此会话移除noFriend会话组
|
||||
final res = await ImService.instance.getConversation(conversationID: 'c2c_$vlogerId');
|
||||
if (res.success) {
|
||||
V2TimConversation conversation = res.data;
|
||||
if (conversation.conversationGroupList?.isNotEmpty == true) {
|
||||
//移除陌生人会话
|
||||
await ImService.instance.deleteConversationsFromGroup(
|
||||
groupName: conversation.conversationGroupList!.first!,
|
||||
conversationIDList: [conversation.conversationID],
|
||||
);
|
||||
//重新构建会话
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('关注'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: FStyle.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -80,20 +80,20 @@ class _VideoPageState extends State<VideoPage> with SingleTickerProviderStateMix
|
||||
backgroundColor: ![0, 1, 2].contains(videoModuleController.videoTabIndex.value) ? null : Colors.transparent,
|
||||
foregroundColor: ![0, 1, 2].contains(videoModuleController.videoTabIndex.value) ? Colors.black : Colors.white,
|
||||
titleSpacing: 1.0,
|
||||
leading: Obx(() => IconButton(
|
||||
icon: Badge.count(
|
||||
backgroundColor: Colors.red,
|
||||
count: 6,
|
||||
child: Icon(
|
||||
Icons.sort_rounded,
|
||||
color: tabColor(),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
// 自定义打开右侧drawer
|
||||
scaffoldKey.currentState?.openDrawer();
|
||||
},
|
||||
)),
|
||||
// leading: Obx(() => IconButton(
|
||||
// icon: Badge.count(
|
||||
// backgroundColor: Colors.red,
|
||||
// count: 6,
|
||||
// child: Icon(
|
||||
// Icons.sort_rounded,
|
||||
// color: tabColor(),
|
||||
// ),
|
||||
// ),
|
||||
// onPressed: () {
|
||||
// // 自定义打开右侧drawer
|
||||
// scaffoldKey.currentState?.openDrawer();
|
||||
// },
|
||||
// )),
|
||||
title: Obx(() {
|
||||
return ScrollConfiguration(
|
||||
behavior: CustomScrollBehavior().copyWith(scrollbars: false),
|
||||
@ -160,30 +160,30 @@ class _VideoPageState extends State<VideoPage> with SingleTickerProviderStateMix
|
||||
),
|
||||
),
|
||||
// 侧边栏
|
||||
drawer: Drawer(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(right: Radius.circular(15.0))),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
width: 300,
|
||||
child: Container(
|
||||
color: Colors.grey[50],
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 80.0,
|
||||
),
|
||||
Icon(
|
||||
Icons.tips_and_updates_outlined,
|
||||
color: Colors.grey,
|
||||
size: 50.0,
|
||||
),
|
||||
Text(
|
||||
'自定义侧边栏~',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// drawer: Drawer(
|
||||
// shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(right: Radius.circular(15.0))),
|
||||
// clipBehavior: Clip.antiAlias,
|
||||
// width: 300,
|
||||
// child: Container(
|
||||
// color: Colors.grey[50],
|
||||
// child: Column(
|
||||
// children: [
|
||||
// SizedBox(
|
||||
// height: 80.0,
|
||||
// ),
|
||||
// Icon(
|
||||
// Icons.tips_and_updates_outlined,
|
||||
// color: Colors.grey,
|
||||
// size: 50.0,
|
||||
// ),
|
||||
// Text(
|
||||
// '自定义侧边栏~',
|
||||
// style: TextStyle(color: Colors.grey, fontSize: 12.0),
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,16 +2,24 @@
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:loopin/IM/controller/chat_controller.dart';
|
||||
import 'package:loopin/IM/im_core.dart';
|
||||
import 'package:loopin/IM/im_message.dart';
|
||||
import 'package:loopin/api/video_api.dart';
|
||||
import 'package:loopin/components/my_toast.dart';
|
||||
import 'package:loopin/components/network_or_asset_image.dart';
|
||||
import 'package:loopin/models/summary_type.dart';
|
||||
import 'package:loopin/service/http.dart';
|
||||
import 'package:loopin/utils/wxsdk.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
|
||||
|
||||
import '../../../behavior/custom_scroll_behavior.dart';
|
||||
import '../../../controller/video_module_controller.dart';
|
||||
@ -40,6 +48,7 @@ class RecommendModule extends StatefulWidget {
|
||||
|
||||
class _RecommendModuleState extends State<RecommendModule> {
|
||||
VideoModuleController videoModuleController = Get.put(VideoModuleController());
|
||||
final ChatController chatController = Get.find<ChatController>();
|
||||
|
||||
// class _RecommendModuleState extends State<RecommendModule> with AutomaticKeepAliveClientMixin {
|
||||
// @override
|
||||
@ -80,10 +89,16 @@ class _RecommendModuleState extends State<RecommendModule> {
|
||||
];
|
||||
// 分享列表
|
||||
List shareList = [
|
||||
{'icon': 'assets/images/share-wx.png', 'label': '好友'},
|
||||
{'icon': 'assets/images/share-wx.png', 'label': '微信'},
|
||||
{'icon': 'assets/images/share-pyq.png', 'label': '朋友圈'},
|
||||
{'icon': 'assets/images/share-link.png', 'label': '复制链接'},
|
||||
{'icon': 'assets/images/share-download.png', 'label': '下载'},
|
||||
{'icon': 'assets/images/share-download.png', 'label': '下载'},
|
||||
{'icon': 'assets/images/share-download.png', 'label': '下载'},
|
||||
{'icon': 'assets/images/share-download.png', 'label': '下载'},
|
||||
{'icon': 'assets/images/share-download.png', 'label': '下载下载下载下载下载下载下载下载下载下载下载下载'},
|
||||
{'icon': 'assets/images/share-download.png', 'label': '下载下载下载下载下载下载下载下载下载下载下载下载'},
|
||||
];
|
||||
|
||||
@override
|
||||
@ -178,10 +193,12 @@ class _RecommendModuleState extends State<RecommendModule> {
|
||||
|
||||
if (data['rows'] is List) {
|
||||
List videos = data['rows'];
|
||||
// for (var item in videos) {
|
||||
for (var item in videos) {
|
||||
// print("喜欢:${item['likeCounts']}");
|
||||
// print("评论:${item['commentsCounts']}");
|
||||
// }
|
||||
// logger.i(item);
|
||||
item['expanded'] = false;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
if (page == 1) {
|
||||
@ -221,6 +238,8 @@ class _RecommendModuleState extends State<RecommendModule> {
|
||||
|
||||
// 评论弹框
|
||||
void handleComment(index) {
|
||||
//获取评论数据
|
||||
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(15.0))),
|
||||
@ -423,46 +442,103 @@ class _RecommendModuleState extends State<RecommendModule> {
|
||||
|
||||
// 分享弹框
|
||||
void handleShare(index) {
|
||||
if (chatController.chatList.isNotEmpty) {
|
||||
chatController.getConversationList();
|
||||
}
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(15.0))),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(15.0)),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) {
|
||||
return Material(
|
||||
color: Colors.white,
|
||||
child: SizedBox(
|
||||
height: 170,
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ScrollConfiguration(
|
||||
behavior: CustomScrollBehavior().copyWith(scrollbars: false),
|
||||
// 分享列表
|
||||
SizedBox(
|
||||
height: 110,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: EdgeInsets.symmetric(vertical: 20.0, horizontal: 10.0),
|
||||
itemCount: shareList.length,
|
||||
padding: EdgeInsets.symmetric(horizontal: 0, vertical: 20.0),
|
||||
itemBuilder: (context, index) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.0),
|
||||
return GestureDetector(
|
||||
onTap: () => handleShareClick(index),
|
||||
child: Container(
|
||||
width: 64,
|
||||
margin: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Column(
|
||||
spacing: 5.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset('${shareList[index]['icon']}', width: 48.0),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
'${shareList[index]['label']}',
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
)
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// 会话列表
|
||||
Obx(() {
|
||||
// 这里过滤掉有分组的会话
|
||||
final filteredList = chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).toList();
|
||||
if (filteredList.isEmpty) return SizedBox.shrink();
|
||||
return SizedBox(
|
||||
height: 110,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: filteredList.length,
|
||||
padding: EdgeInsets.symmetric(horizontal: 0, vertical: 20.0),
|
||||
itemBuilder: (context, index) {
|
||||
return GestureDetector(
|
||||
// 点击分享
|
||||
onTap: () => handlCoverClick(filteredList[index].conversation),
|
||||
child: Container(
|
||||
width: 64,
|
||||
margin: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Image.asset('${chatController.chatList[index].faceUrl}', width: 48.0),
|
||||
NetworkOrAssetImage(
|
||||
imageUrl: filteredList[index].faceUrl,
|
||||
width: 48.0,
|
||||
height: 48.0,
|
||||
),
|
||||
InkWell(
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
'${filteredList[index].conversation.showName}',
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// 取消按钮
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: InkWell(
|
||||
onTap: () => Get.back(),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
width: double.infinity,
|
||||
@ -473,9 +549,7 @@ class _RecommendModuleState extends State<RecommendModule> {
|
||||
style: TextStyle(color: Colors.black87),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -485,6 +559,52 @@ class _RecommendModuleState extends State<RecommendModule> {
|
||||
);
|
||||
}
|
||||
|
||||
void handleShareClick(int index) {
|
||||
print("分享项 $index 被点击");
|
||||
final description = videoList[videoModuleController.videoPlayIndex.value]['title'] ?? '获取title失败';
|
||||
|
||||
if (index == 1) {
|
||||
// 好友
|
||||
Wxsdk.shareToFriend(title: '快来看看这个视频', description: description, webpageUrl: 'https://baidu.com');
|
||||
} else if (index == 2) {
|
||||
// 朋友圈
|
||||
Wxsdk.shareToTimeline(title: '快来看看这个视频', webpageUrl: 'https://baidu.com');
|
||||
}
|
||||
}
|
||||
|
||||
void handlCoverClick(V2TimConversation conv) async {
|
||||
// 发送VideoMsg,获取当前视频信息
|
||||
final userId = conv.userID;
|
||||
final String url = videoList[videoModuleController.videoPlayIndex.value]['url'];
|
||||
final img = videoList[videoModuleController.videoPlayIndex.value]['firstFrameImg'];
|
||||
final width = videoList[videoModuleController.videoPlayIndex.value]['width'];
|
||||
final height = videoList[videoModuleController.videoPlayIndex.value]['height'];
|
||||
final makeJson = jsonEncode({
|
||||
"width": width,
|
||||
"height": height,
|
||||
"imgUrl": img,
|
||||
"videoUrl": url,
|
||||
});
|
||||
final res = await IMMessage().createCustomMessage(
|
||||
data: makeJson,
|
||||
);
|
||||
if (res.success) {
|
||||
final sendRes = await IMMessage().sendMessage(msg: res.data!.messageInfo!, toUserID: userId, cloudCustomData: SummaryType.shareVideo);
|
||||
if (sendRes.success) {
|
||||
MyToast().tip(
|
||||
title: '分享成功',
|
||||
position: 'center',
|
||||
type: 'success',
|
||||
);
|
||||
Get.back();
|
||||
} else {
|
||||
logger.e(res.desc);
|
||||
}
|
||||
} else {
|
||||
logger.e(res.desc);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// super.build(context);
|
||||
@ -601,6 +721,11 @@ class _RecommendModuleState extends State<RecommendModule> {
|
||||
SizedBox(
|
||||
height: 55.0,
|
||||
width: 48.0,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
player.pause();
|
||||
Get.toNamed('/vloger', arguments: videoList[videoModuleController.videoPlayIndex.value]);
|
||||
},
|
||||
child: UnconstrainedBox(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(
|
||||
@ -611,8 +736,10 @@ class _RecommendModuleState extends State<RecommendModule> {
|
||||
borderRadius: BorderRadius.circular(100.0),
|
||||
),
|
||||
child: ClipOval(
|
||||
child:
|
||||
Image.network(videoList[index]['vlogerFace'] ?? 'https://wuzhongjie.com.cn/download/logo.png', fit: BoxFit.cover),
|
||||
child: NetworkOrAssetImage(
|
||||
imageUrl: videoList[index]['avatar'],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -643,6 +770,7 @@ class _RecommendModuleState extends State<RecommendModule> {
|
||||
),
|
||||
],
|
||||
),
|
||||
// 点赞
|
||||
GestureDetector(
|
||||
child: Column(
|
||||
children: [
|
||||
@ -664,6 +792,7 @@ class _RecommendModuleState extends State<RecommendModule> {
|
||||
});
|
||||
},
|
||||
),
|
||||
// 评论
|
||||
GestureDetector(
|
||||
child: Column(
|
||||
children: [
|
||||
@ -697,6 +826,7 @@ class _RecommendModuleState extends State<RecommendModule> {
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// 转发
|
||||
GestureDetector(
|
||||
child: Column(
|
||||
children: [
|
||||
@ -706,16 +836,28 @@ class _RecommendModuleState extends State<RecommendModule> {
|
||||
height: 40.0,
|
||||
width: 40.0,
|
||||
),
|
||||
// Text(
|
||||
// '${videoList[index]['shareNum']}',
|
||||
// style: TextStyle(color: Colors.white, fontSize: 12.0),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
handleShare(index);
|
||||
},
|
||||
),
|
||||
//举报
|
||||
GestureDetector(
|
||||
child: Column(
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/images/svg/report.svg',
|
||||
colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
|
||||
height: 40.0,
|
||||
width: 40.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
// 举报
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -726,19 +868,64 @@ class _RecommendModuleState extends State<RecommendModule> {
|
||||
right: 80.0,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 5.0,
|
||||
children: [
|
||||
Text(
|
||||
'@${videoList[index]['vlogerName'] ?? '未知'}',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16.0),
|
||||
'@${videoList[videoModuleController.videoPlayIndex.value]['nickname'] ?? '未知'}',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16.0),
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final text = videoList[videoModuleController.videoPlayIndex.value]['title'] ?? '未知';
|
||||
// 先用 TextPainter 判断是否超过 3 行
|
||||
final span = TextSpan(
|
||||
text: text,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14.0),
|
||||
);
|
||||
final tp = TextPainter(
|
||||
text: span,
|
||||
maxLines: 3,
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
tp.layout(maxWidth: constraints.maxWidth);
|
||||
final isOverflow = tp.didExceedMaxLines;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${videoList[index]['content'] ?? '未知'}',
|
||||
style: TextStyle(color: Colors.white, fontSize: 14.0),
|
||||
text,
|
||||
maxLines: videoList[videoModuleController.videoPlayIndex.value]['expanded'] ? null : 3,
|
||||
overflow:
|
||||
videoList[videoModuleController.videoPlayIndex.value]['expanded'] ? TextOverflow.visible : TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14.0),
|
||||
),
|
||||
if (isOverflow)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6.0),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
videoList[videoModuleController.videoPlayIndex.value]['expanded'] =
|
||||
!videoList[videoModuleController.videoPlayIndex.value]['expanded'];
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
videoList[videoModuleController.videoPlayIndex.value]['expanded'] ? '收起' : '展开更多',
|
||||
textAlign: TextAlign.right,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
// mini播放进度条
|
||||
Positioned(
|
||||
bottom: 0.0,
|
||||
|
@ -3,7 +3,15 @@ library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:loopin/bings/chat_binding.dart';
|
||||
import 'package:loopin/pages/chat/chat.dart';
|
||||
import 'package:loopin/pages/chat/chat_group.dart';
|
||||
import 'package:loopin/pages/chat/chat_no_friend.dart';
|
||||
import 'package:loopin/pages/my/des.dart';
|
||||
import 'package:loopin/pages/my/nick_name.dart';
|
||||
import 'package:loopin/pages/my/setting.dart';
|
||||
import 'package:loopin/pages/my/user_info.dart';
|
||||
import 'package:loopin/pages/my/vloger.dart';
|
||||
|
||||
import '../layouts/index.dart';
|
||||
/* 引入路由页面 */
|
||||
@ -19,25 +27,61 @@ import '../utils/common.dart';
|
||||
// 路由地址集合
|
||||
final Map<String, Widget> routes = {
|
||||
'/': const Layout(),
|
||||
// '/upload': const UploadVideoPage(),
|
||||
'/goods': const Goods(),
|
||||
'/chat': const Chat(),
|
||||
// '/chat': const Chat(),
|
||||
// '/chatNoFriend': const ChatNoFriend(),
|
||||
// '/chatGroup': const ChatGroup(),
|
||||
'/order': const Order(),
|
||||
'/order/detail': const OrderDetail(),
|
||||
'/vloger': const Vloger(),
|
||||
//settins
|
||||
'/setting': const Setting(),
|
||||
'/userInfo': const UserInfo(),
|
||||
'/notifications': const Setting(),
|
||||
'/privacy': const Setting(),
|
||||
'/about': const Setting(),
|
||||
'/des': const Des(),
|
||||
'/nickName': const NickName(),
|
||||
};
|
||||
|
||||
final List<GetPage> routeList = routes.entries
|
||||
.map((e) => GetPage(
|
||||
name: e.key, // 路由名称
|
||||
page: () => e.value, // 路由页面
|
||||
transition: Transition.rightToLeftWithFade, // 跳转路由动画
|
||||
// transition: Transition.rightToLeftWithFade, // 跳转路由动画
|
||||
transition: Transition.rightToLeft, // 跳转路由动画
|
||||
middlewares: [RouteMiddleware()], // 路由中间件
|
||||
))
|
||||
.toList();
|
||||
|
||||
final List<GetPage> bingsRoutes = [
|
||||
GetPage(
|
||||
name: '/chat',
|
||||
page: () => const Chat(),
|
||||
binding: ChatBinding(),
|
||||
transition: Transition.rightToLeft,
|
||||
middlewares: [RouteMiddleware()],
|
||||
),
|
||||
GetPage(
|
||||
name: '/chatNoFriend',
|
||||
page: () => const ChatNoFriend(),
|
||||
binding: ChatBinding(),
|
||||
transition: Transition.rightToLeft,
|
||||
middlewares: [RouteMiddleware()],
|
||||
),
|
||||
GetPage(
|
||||
name: '/chatGroup',
|
||||
page: () => const ChatGroup(),
|
||||
binding: ChatBinding(),
|
||||
transition: Transition.rightToLeft,
|
||||
middlewares: [RouteMiddleware()],
|
||||
),
|
||||
];
|
||||
|
||||
final List<GetPage> routePages = [
|
||||
GetPage(name: '/login', page: () => const Login()),
|
||||
...routeList,
|
||||
...bingsRoutes,
|
||||
];
|
||||
|
||||
// 路由中间件拦截验证
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:loopin/IM/push_service.dart';
|
||||
|
||||
import 'http_config.dart';
|
||||
|
||||
@ -15,12 +16,16 @@ class Http {
|
||||
}
|
||||
|
||||
static Future<dynamic> post(String url, {dynamic data, Map<String, dynamic>? headers}) async {
|
||||
try {
|
||||
final res = await _dio.post(
|
||||
url,
|
||||
data: data,
|
||||
options: Options(extra: headers ?? {}),
|
||||
);
|
||||
return res.data;
|
||||
} catch (e) {
|
||||
logger.e('$e--------$url');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<dynamic> put(String url, {dynamic data, Map<String, dynamic>? headers}) async {
|
||||
|
@ -6,10 +6,11 @@ import 'package:get_storage/get_storage.dart';
|
||||
class HttpConfig {
|
||||
static final Dio dio = Dio(BaseOptions(
|
||||
// baseUrl: 'http://43.143.227.203:8099',
|
||||
baseUrl: 'http://111.62.22.190:8080',
|
||||
// baseUrl: 'http://111.62.22.190:8080',
|
||||
// baseUrl: 'http://cjh.wuzhongjie.com.cn',
|
||||
connectTimeout: Duration(seconds: 30),
|
||||
receiveTimeout: Duration(seconds: 30),
|
||||
baseUrl: 'http://82.156.121.2:8880',
|
||||
// connectTimeout: Duration(seconds: 30),
|
||||
// receiveTimeout: Duration(seconds: 30),
|
||||
));
|
||||
|
||||
static final box = GetStorage();
|
||||
@ -33,13 +34,14 @@ class HttpConfig {
|
||||
handler.next(options);
|
||||
},
|
||||
onResponse: (response, handler) {
|
||||
// logger.e(response.requestOptions.data);
|
||||
final data = response.data;
|
||||
// data['code'] = 200; // 旧接口测试用
|
||||
if (data is Map<String, dynamic>) {
|
||||
if (data['code'] != 200) {
|
||||
Get.snackbar(
|
||||
'错误',
|
||||
data['msg'] ?? '请求失败',
|
||||
'错误码${data['code']}',
|
||||
'${response.requestOptions.uri}\n${response.requestOptions.data}\n${data['msg']}' ?? '请求失败',
|
||||
duration: Duration(minutes: 1),
|
||||
backgroundColor: Colors.red.withAlpha(230),
|
||||
colorText: Colors.white,
|
||||
icon: const Icon(Icons.error_outline, color: Colors.white),
|
||||
|
@ -27,7 +27,7 @@ class UpgradeDialog extends StatelessWidget {
|
||||
children: [
|
||||
Image.asset('assets/images/update/rocket.png', width: 80, height: 80),
|
||||
const SizedBox(height: 12),
|
||||
Text("发现新版本 v$version", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
Text("发现新版本 $version", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 10),
|
||||
// Text(content, style: TextStyle(fontSize: 14)),
|
||||
Column(
|
||||
|
@ -1,52 +1,64 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:loopin/IM/im_core.dart';
|
||||
import 'package:loopin/api/common_api.dart';
|
||||
import 'package:loopin/service/http.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
// import 'package:loopin/api/common_api.dart';
|
||||
// import 'package:loopin/service/http.dart';
|
||||
|
||||
import 'upgrade_dialog.dart';
|
||||
import 'upgrade_util.dart';
|
||||
|
||||
class UpgradeService {
|
||||
static Future<void> checkUpgrade(BuildContext context) async {
|
||||
static Future<void> checkUpgrade(State state) async {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
print('App version: ${info.version}');
|
||||
print('version_code: ${info.buildNumber}');
|
||||
final currentVersion = info.version;
|
||||
// final res = await Http.get(CommonApi.checkVersion);
|
||||
// final data = res['data'];
|
||||
final data = {
|
||||
"version": "4.1.0",
|
||||
"content": [
|
||||
"新增火箭弹窗",
|
||||
"修复若干 Bug",
|
||||
"优化界面动画",
|
||||
],
|
||||
"force": 0,
|
||||
"apkUrl": "https://wuzhongjie.com.cn/download/wzj.apk",
|
||||
"iosUrl": "https://apps.apple.com/cn/app/无终街/id6479185362",
|
||||
};
|
||||
if (!state.mounted) return;
|
||||
|
||||
logger.i('App version: ${info.version}');
|
||||
logger.i('version_code: ${info.buildNumber}');
|
||||
final res = await Http.post(CommonApi.checkVersion, data: {
|
||||
'platformType': Platform.isAndroid ? 'android' : 'ios',
|
||||
'status': 1,
|
||||
});
|
||||
if (!state.mounted) return;
|
||||
|
||||
// logger.i(res);
|
||||
final result = res['data']['records'] as List;
|
||||
final data = result.first;
|
||||
final currentVersion = info.buildNumber;
|
||||
if (currentVersion != data['versionCode']) {
|
||||
// 版本号不一致
|
||||
// 0 表示 false非强制,非 0 表示 true强制
|
||||
final bool force = (data['force'] ?? 0) != 0;
|
||||
final bool force = (data['isForceUpdate'] ?? 0) != 0;
|
||||
// 弹窗
|
||||
showDialog(
|
||||
context: context,
|
||||
context: state.context,
|
||||
barrierDismissible: !force,
|
||||
builder: (_) => UpgradeDialog(
|
||||
version: data['version']?.toString() ?? '',
|
||||
content: (data['content'] as List<dynamic>).map((e) => e.toString()).toList(),
|
||||
version: data['versionName']?.toString() ?? '',
|
||||
content: (data['releaseNotes'] as String).split(',').map((e) => e.trim()).toList(),
|
||||
force: force,
|
||||
onConfirm: () {
|
||||
if (Platform.isAndroid) {
|
||||
Navigator.pop(context);
|
||||
UpgradeUtil.downloadAndInstallAPK(context, data['apkUrl']?.toString() ?? '');
|
||||
Navigator.pop(state.context);
|
||||
UpgradeUtil.downloadAndInstallAPK(state.context, data['downloadUrl']?.toString() ?? '');
|
||||
} else if (Platform.isIOS) {
|
||||
UpgradeUtil.launchAppStore(data['iosUrl']?.toString() ?? '');
|
||||
UpgradeUtil.launchAppStore(data['downloadUrl']?.toString() ?? '');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
// final data = {
|
||||
// "version": "4.1.0",
|
||||
// "content": [
|
||||
// "新增火箭弹窗",
|
||||
// "修复若干 Bug",
|
||||
// "优化界面动画",
|
||||
// ],
|
||||
// "force": 0,
|
||||
// "apkUrl": "https://wuzhongjie.com.cn/download/wzj.apk",
|
||||
// "iosUrl": "https://apps.apple.com/cn/app/无终街/id6479185362",
|
||||
// };
|
||||
}
|
||||
}
|
||||
|
57
lib/utils/audio_player_service.dart
Normal file
@ -0,0 +1,57 @@
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:loopin/IM/im_core.dart';
|
||||
|
||||
class AudioPlayerService {
|
||||
static final AudioPlayerService _instance = AudioPlayerService._internal();
|
||||
factory AudioPlayerService() => _instance;
|
||||
AudioPlayerService._internal();
|
||||
|
||||
final AudioPlayer _audioPlayer = AudioPlayer();
|
||||
|
||||
/// 播放本地文件
|
||||
Future<void> playLocal(String filePath) async {
|
||||
try {
|
||||
await _audioPlayer.setSourceDeviceFile(filePath);
|
||||
await _audioPlayer.resume();
|
||||
} catch (e) {
|
||||
logger.e('播放本地音频失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 播放网络音频
|
||||
Future<void> playNetwork(String url) async {
|
||||
try {
|
||||
await _audioPlayer.setSourceUrl(url);
|
||||
await _audioPlayer.resume();
|
||||
} catch (e) {
|
||||
logger.e('播放网络音频失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 暂停播放
|
||||
Future<void> pause() async {
|
||||
try {
|
||||
await _audioPlayer.pause();
|
||||
} catch (e) {
|
||||
logger.e('暂停播放失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止播放
|
||||
Future<void> stop() async {
|
||||
try {
|
||||
await _audioPlayer.stop();
|
||||
} catch (e) {
|
||||
logger.e('停止播放失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 释放资源
|
||||
Future<void> dispose() async {
|
||||
try {
|
||||
await _audioPlayer.dispose();
|
||||
} catch (e) {
|
||||
logger.e('释放播放器失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ import './storage.dart';
|
||||
|
||||
class Common {
|
||||
/// 判断是否登录
|
||||
static isLogin() {
|
||||
static bool isLogin() {
|
||||
return Storage.hasData('hasLogged');
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ class Utils {
|
||||
}
|
||||
|
||||
/// 是否为空
|
||||
static isEmpty(val) {
|
||||
static bool isEmpty(val) {
|
||||
if (val == null) return true;
|
||||
if (val is bool && val == false) return true;
|
||||
if (val is String) return val.isEmpty;
|
||||
@ -87,6 +87,7 @@ class Utils {
|
||||
|
||||
//翻译媒体库title
|
||||
static String translateAlbumName(AssetPathEntity album) {
|
||||
// logger.i(album.name);
|
||||
final Map<String, String> albumNameMap = {
|
||||
'recents': '最近项目',
|
||||
'favorites': '收藏',
|
||||
@ -105,6 +106,8 @@ class Utils {
|
||||
'animated': '动图',
|
||||
'raw': 'RAW格式',
|
||||
'hidden': '已隐藏',
|
||||
'recent': '最近项目', // 安卓
|
||||
'camera': '相机', // 安卓
|
||||
};
|
||||
return albumNameMap[album.name.toLowerCase()] ?? album.name;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import 'package:loopin/IM/im_service.dart';
|
||||
import 'package:loopin/utils/parse_message_summary.dart';
|
||||
import 'package:shirne_dialog/shirne_dialog.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart';
|
||||
|
||||
class NotificationBanner {
|
||||
static void show(V2TimMessage msg) {
|
||||
@ -49,4 +50,32 @@ class NotificationBanner {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 被关注通知
|
||||
static void foucs(V2TimUserFullInfo msg) {
|
||||
final nickname = msg.nickName ?? '未知用户';
|
||||
final avatar = msg.faceUrl ?? '';
|
||||
final text = '$nickname:关注了你';
|
||||
|
||||
Get.snackbar(
|
||||
'新的关注',
|
||||
text,
|
||||
duration: const Duration(seconds: 5),
|
||||
snackPosition: SnackPosition.TOP,
|
||||
margin: const EdgeInsets.all(12),
|
||||
backgroundColor: Get.theme.cardColor,
|
||||
colorText: Get.theme.textTheme.bodyLarge?.color,
|
||||
icon: avatar.isNotEmpty
|
||||
? CircleAvatar(
|
||||
backgroundImage: NetworkImage(avatar),
|
||||
radius: 16,
|
||||
)
|
||||
: null,
|
||||
onTap: (_) async {
|
||||
// 点击后立刻关闭
|
||||
Get.closeCurrentSnackbar();
|
||||
// 跳转到新关注我的页面
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:loopin/models/summary_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
|
||||
|
||||
@ -14,37 +15,38 @@ String parseMessageSummary(V2TimMessage msg) {
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_VIDEO:
|
||||
return '[视频]';
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_FILE:
|
||||
return '[文件]';
|
||||
return '[文件]'; // 先不做
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_LOCATION:
|
||||
return '[位置]';
|
||||
return '[位置]'; // 先不做
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_FACE:
|
||||
return '[表情]';
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_CUSTOM:
|
||||
return _parseCustomMessage(msg.customElem?.data);
|
||||
return _parseCustomMessage(msg);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_MERGER:
|
||||
return '[合并转发消息]';
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS:
|
||||
return '[群提示]';
|
||||
default:
|
||||
return '[未知消息类型]';
|
||||
return '[未知消息类型1]';
|
||||
}
|
||||
}
|
||||
|
||||
String _parseCustomMessage(String? data) {
|
||||
if (data == null || data.isEmpty) return '[自定义消息]';
|
||||
|
||||
String _parseCustomMessage(V2TimMessage? msg) {
|
||||
if (msg == null) return '[null]';
|
||||
final sum = msg.cloudCustomData;
|
||||
try {
|
||||
final jsonData = json.decode(data);
|
||||
final type = jsonData['type'];
|
||||
switch (type) {
|
||||
case 'interaction':
|
||||
return '[互动] ${jsonData['action'] ?? ''}';
|
||||
case 'forward':
|
||||
return '[转发] ${jsonData['title'] ?? ''}';
|
||||
switch (sum) {
|
||||
case SummaryType.hongbao:
|
||||
final hbData = jsonDecode(msg.customElem!.data!);
|
||||
return '[红包]${hbData['remark']}';
|
||||
case SummaryType.shareTuangou:
|
||||
return '[分享商品]';
|
||||
case SummaryType.shareVideo:
|
||||
return '[分享视频]';
|
||||
default:
|
||||
return '[自定义消息]';
|
||||
return '[未知消息类型2]';
|
||||
}
|
||||
} catch (_) {
|
||||
return '[自定义消息]';
|
||||
return '[未知消息类型3]';
|
||||
}
|
||||
}
|
||||
|
15
lib/utils/snapshot.dart
Normal file
@ -0,0 +1,15 @@
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:video_thumbnail/video_thumbnail.dart';
|
||||
|
||||
/// 视频首帧截取
|
||||
Future<String?> generateVideoThumbnail(String videoPath) async {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final thumbnailPath = await VideoThumbnail.thumbnailFile(
|
||||
video: videoPath,
|
||||
thumbnailPath: tempDir.path,
|
||||
imageFormat: ImageFormat.JPEG,
|
||||
maxWidth: 120,
|
||||
quality: 75,
|
||||
);
|
||||
return thumbnailPath;
|
||||
}
|
80
lib/utils/voice_service.dart
Normal file
@ -0,0 +1,80 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:loopin/IM/im_core.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:record/record.dart';
|
||||
|
||||
class VoiceService {
|
||||
static final VoiceService _instance = VoiceService._internal();
|
||||
factory VoiceService() => _instance;
|
||||
VoiceService._internal();
|
||||
|
||||
final AudioRecorder _recorder = AudioRecorder();
|
||||
String? _voiceFilePath;
|
||||
DateTime? _startTime;
|
||||
|
||||
/// 开始录音
|
||||
Future<bool> startRecording() async {
|
||||
if (await _recorder.hasPermission()) {
|
||||
final dir = await getTemporaryDirectory(); // 临时目录
|
||||
final filePath = '${dir.path}/${DateTime.now().millisecondsSinceEpoch}.m4a';
|
||||
_voiceFilePath = filePath;
|
||||
_startTime = DateTime.now();
|
||||
|
||||
await _recorder.start(
|
||||
const RecordConfig(
|
||||
encoder: AudioEncoder.aacLc,
|
||||
bitRate: 128000,
|
||||
sampleRate: 44100,
|
||||
),
|
||||
path: filePath,
|
||||
);
|
||||
logger.i('开始录音,文件路径: $filePath');
|
||||
|
||||
return true;
|
||||
} else {
|
||||
logger.e("没有录音权限");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止录音并返回文件路径
|
||||
Future<Map<String, dynamic>?> stopRecording() async {
|
||||
await _recorder.stop();
|
||||
|
||||
if (_voiceFilePath != null && File(_voiceFilePath!).existsSync() && _startTime != null) {
|
||||
final duration = DateTime.now().difference(_startTime!);
|
||||
final durationSeconds = duration.inSeconds;
|
||||
if (durationSeconds < 1 || durationSeconds > 60) {
|
||||
logger.w('录音时长不在允许范围(1-60秒),删除文件: $_voiceFilePath,时长: $durationSeconds 秒');
|
||||
try {
|
||||
await File(_voiceFilePath!).delete();
|
||||
} catch (e) {
|
||||
logger.e('删除录音文件失败: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
logger.i('录音完成: $_voiceFilePath,时长: ${duration.inSeconds}秒');
|
||||
return {
|
||||
'path': _voiceFilePath,
|
||||
'duration': duration.inMilliseconds,
|
||||
};
|
||||
}
|
||||
logger.e("没有录到音频文件");
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 取消录音(删除文件)
|
||||
Future<void> cancelRecording() async {
|
||||
await _recorder.stop();
|
||||
if (_voiceFilePath != null) {
|
||||
final file = File(_voiceFilePath!);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
logger.i('录音已取消并删除');
|
||||
}
|
||||
}
|
||||
_voiceFilePath = null;
|
||||
}
|
||||
}
|
144
lib/utils/wxsdk.dart
Normal file
@ -0,0 +1,144 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:fluwx/fluwx.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:loopin/IM/controller/im_user_info_controller.dart';
|
||||
import 'package:loopin/IM/im_service.dart';
|
||||
import 'package:loopin/api/common_api.dart';
|
||||
import 'package:loopin/service/http.dart';
|
||||
|
||||
class Wxsdk {
|
||||
static bool _inited = false;
|
||||
|
||||
static final Fluwx fluwx = Fluwx();
|
||||
|
||||
static Future<bool> init() async {
|
||||
if (_inited) {
|
||||
return true;
|
||||
}
|
||||
_inited = true;
|
||||
final initRes = await fluwx.registerApi(
|
||||
appId: 'wxebcdaea31881caab',
|
||||
doOnAndroid: true,
|
||||
doOnIOS: true,
|
||||
universalLink: 'https://wuzhongjie.com.cn/',
|
||||
);
|
||||
if (initRes) {
|
||||
logger.i('微信sdk初始化成功');
|
||||
// 全局监听授权回调
|
||||
fluwx.addSubscriber(
|
||||
(res) async {
|
||||
//授权
|
||||
if (res is WeChatAuthResponse) {
|
||||
if (res.isSuccessful) {
|
||||
final code = res.code;
|
||||
logger.i('微信回调,code: $code,类型:${res.state}');
|
||||
if (res.state == 'getOpenId') {
|
||||
// TODO: 使用 code 向后台换取 access_token、unionid
|
||||
final serverRes = await Http.post(CommonApi.wxLogin, data: {
|
||||
"source": "wechat_open",
|
||||
"socialCode": "${res.code}",
|
||||
"socialState": "1",
|
||||
"clientId": "428a8310cd442757ae699df5d894f051",
|
||||
"grantType": "social"
|
||||
});
|
||||
final info = Get.find<ImUserInfoController>();
|
||||
info.customInfo['openId'] = serverRes['data']['openId'];
|
||||
info.updateOpenId();
|
||||
info.customInfo.refresh();
|
||||
logger.w(serverRes['data']['openId']);
|
||||
}
|
||||
} else {
|
||||
logger.w('微信授权失败: ${res.errStr}-类型:${res.state}');
|
||||
}
|
||||
}
|
||||
// 分享
|
||||
if (res is WeChatShareResponse) {
|
||||
logger.w(res.isSuccessful);
|
||||
// 这里只能确保打开了微信,是取消了还是确认了没办法知道
|
||||
if (res.isSuccessful) {}
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
logger.i('微信SDK初始化失败:$initRes');
|
||||
}
|
||||
return initRes;
|
||||
}
|
||||
|
||||
/// 调用微信登录
|
||||
static Future<void> login() async {
|
||||
final result = await fluwx.authBy(
|
||||
which: NormalAuth(
|
||||
scope: 'snsapi_userinfo',
|
||||
state: 'getOpenId',
|
||||
),
|
||||
);
|
||||
if (!result) {
|
||||
logger.e('微信授权请求发送失败');
|
||||
}
|
||||
}
|
||||
|
||||
///分享好友
|
||||
static Future<bool> shareToFriend({
|
||||
required String title,
|
||||
required String description,
|
||||
required String webpageUrl,
|
||||
String thumbnailAssetPath = 'assets/images/logo/logo.png',
|
||||
}) async {
|
||||
Uint8List? thumbData;
|
||||
thumbData = await _loadLocalThumbnail(thumbnailAssetPath);
|
||||
final model = WeChatShareWebPageModel(
|
||||
webpageUrl,
|
||||
title: title,
|
||||
description: description,
|
||||
thumbData: thumbData,
|
||||
scene: WeChatScene.session,
|
||||
);
|
||||
return Fluwx().share(model);
|
||||
}
|
||||
|
||||
///分享到朋友圈
|
||||
static Future<bool> shareToTimeline({
|
||||
required String title,
|
||||
required String webpageUrl,
|
||||
String thumbnailAssetPath = 'assets/images/logo/logo.png',
|
||||
}) async {
|
||||
Uint8List? thumbData;
|
||||
thumbData = await _loadLocalThumbnail(thumbnailAssetPath);
|
||||
final model = WeChatShareWebPageModel(
|
||||
webpageUrl,
|
||||
title: title,
|
||||
thumbData: thumbData,
|
||||
scene: WeChatScene.timeline,
|
||||
);
|
||||
return Fluwx().share(model);
|
||||
}
|
||||
|
||||
static Future<Uint8List> _loadLocalThumbnail(String assetPath) async {
|
||||
final byteData = await rootBundle.load(assetPath);
|
||||
final originBytes = byteData.buffer.asUint8List();
|
||||
|
||||
final compressedBytes = await FlutterImageCompress.compressWithList(
|
||||
originBytes,
|
||||
minWidth: 120, // 微信要求120X120且小于32kb
|
||||
minHeight: 120,
|
||||
quality: 80, // 控制质量
|
||||
format: CompressFormat.jpeg, // 转为 JPEG
|
||||
);
|
||||
|
||||
logger.i("thumbData size: ${compressedBytes.length} bytes");
|
||||
|
||||
return compressedBytes;
|
||||
}
|
||||
|
||||
/// 跳转小程序
|
||||
static Future<void> openMiniApp({required orderId}) async {
|
||||
var miniProgram = MiniProgram(
|
||||
username: "gh_2ffaecc5508e", // 小程序原始ID
|
||||
path: "/pages/index/index?id=$orderId", // 打开时带的路径参数
|
||||
miniProgramType: WXMiniProgramType.test,
|
||||
);
|
||||
Fluwx().open(target: miniProgram);
|
||||
}
|
||||
}
|
256
pubspec.lock
@ -41,6 +41,62 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.12.0"
|
||||
audioplayers:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audioplayers
|
||||
sha256: e653f162ddfcec1da2040ba2d8553fff1662b5c2a5c636f4c21a3b11bee497de
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
audioplayers_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_android
|
||||
sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.2.1"
|
||||
audioplayers_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_darwin
|
||||
sha256: "0811d6924904ca13f9ef90d19081e4a87f7297ddc19fc3d31f60af1aaafee333"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
audioplayers_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_linux
|
||||
sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.2.1"
|
||||
audioplayers_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_platform_interface
|
||||
sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.1.1"
|
||||
audioplayers_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_web
|
||||
sha256: "1c0f17cec68455556775f1e50ca85c40c05c714a99c5eb1d2d57cc17ba5522d7"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
audioplayers_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_windows
|
||||
sha256: "4048797865105b26d47628e6abb49231ea5de84884160229251f37dfcbe52fd7"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.2.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -49,6 +105,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
bottom_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: bottom_picker
|
||||
sha256: b83c35861314aafdef6857be1a8d900d82fa90c979a12af9b653d5d9e7d35beb
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
card_swiper:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -73,6 +137,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
city_pickers:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: city_pickers
|
||||
sha256: "583102c8d9eecb1f7abc5ff52a22d7cb019b9808cdb24b80c7692c769f8da153"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -177,6 +249,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
easy_refresh:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: easy_refresh
|
||||
sha256: "486e30abfcaae66c0f2c2798a10de2298eb9dc5e0bb7e1dba9328308968cae0c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.4.0"
|
||||
extended_image:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -270,6 +350,62 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "10.0.1"
|
||||
flutter_html:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_html
|
||||
sha256: "38a2fd702ffdf3243fb7441ab58aa1bc7e6922d95a50db76534de8260638558d"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
flutter_image_compress:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_image_compress
|
||||
sha256: "51d23be39efc2185e72e290042a0da41aed70b14ef97db362a6b5368d0523b27"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
flutter_image_compress_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_image_compress_common
|
||||
sha256: c5c5d50c15e97dd7dc72ff96bd7077b9f791932f2076c5c5b6c43f2c88607bfb
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
flutter_image_compress_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_image_compress_macos
|
||||
sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
flutter_image_compress_ohos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_image_compress_ohos
|
||||
sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.0.3"
|
||||
flutter_image_compress_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_image_compress_platform_interface
|
||||
sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
flutter_image_compress_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_image_compress_web
|
||||
sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.1.5"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -341,6 +477,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
fluwx:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fluwx
|
||||
sha256: "9db31d54043363c9c8283b5f0bc4df982edb45ba19d800df9d7de96a205371ae"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.7.0"
|
||||
form_builder_validators:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -581,6 +725,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
list_counter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: list_counter
|
||||
sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -597,6 +749,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
lpinyin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lpinyin
|
||||
sha256: "0bb843363f1f65170efd09fbdfc760c7ec34fc6354f9fcb2f89e74866a0d814a"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -749,6 +909,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_drawing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_drawing
|
||||
sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -917,6 +1085,70 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.5"
|
||||
record:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: record
|
||||
sha256: daeb3f9b3fea9797094433fe6e49a879d8e4ca4207740bc6dc7e4a58764f0817
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
record_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_android
|
||||
sha256: "97d7122455f30de89a01c6c244c839085be6b12abca251fc0e78f67fed73628b"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
record_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_ios
|
||||
sha256: "73706ebbece6150654c9d6f57897cf9b622c581148304132ba85dba15df0fdfb"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
record_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_linux
|
||||
sha256: "0626678a092c75ce6af1e32fe7fd1dea709b92d308bc8e3b6d6348e2430beb95"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
record_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_macos
|
||||
sha256: "02240833fde16c33fcf2c589f3e08d4394b704761b4a3bb609d872ff3043fbbd"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
record_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_platform_interface
|
||||
sha256: c1ad38f51e4af88a085b3e792a22c685cb3e7c23fc37aa7ce44c4cf18f25fe89
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
record_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_web
|
||||
sha256: a12856d0b3dd03d336b4b10d7520a8b3e21649a06a8f95815318feaa8f07adbb
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.9"
|
||||
record_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_windows
|
||||
sha256: "85a22fc97f6d73ecd67c8ba5f2f472b74ef1d906f795b7970f771a0914167e99"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
safe_local_storage:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -941,6 +1173,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
scrollable_positioned_list:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: scrollable_positioned_list
|
||||
sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.3.8"
|
||||
shirne_dialog:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1002,6 +1242,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
tencent_cloud_chat_push:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tencent_cloud_chat_push
|
||||
sha256: "7a76d107715e99fd4ed11489b9aa662e2f22d3f4614d9cee72e3f9d97c6f8b0f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "8.6.7019+1"
|
||||
tencent_cloud_chat_sdk:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1202,6 +1450,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.5"
|
||||
video_thumbnail:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_thumbnail
|
||||
sha256: "181a0c205b353918954a881f53a3441476b9e301641688a581e0c13f00dc588b"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.5.6"
|
||||
visibility_detector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
18
pubspec.yaml
@ -41,8 +41,11 @@ dependencies:
|
||||
cupertino_icons: ^1.0.8
|
||||
card_swiper: ^3.0.1
|
||||
flutter_svg: ^2.0.16
|
||||
easy_refresh: ^3.4.0
|
||||
# 腾讯IM
|
||||
tencent_cloud_chat_sdk: ^8.6.7019+2
|
||||
# 离线推送
|
||||
tencent_cloud_chat_push: ^8.6.7019+1
|
||||
# 瀑布流组件
|
||||
flutter_staggered_grid_view: ^0.7.0
|
||||
# 状态管理
|
||||
@ -67,14 +70,23 @@ dependencies:
|
||||
|
||||
wechat_assets_picker: ^9.5.1
|
||||
device_info_plus: ^11.5.0
|
||||
photo_manager: ^3.7.1
|
||||
photo_manager: ^3.7.1 #翻译媒体库
|
||||
flutter_form_builder: ^10.0.1
|
||||
form_builder_validators: ^11.1.2
|
||||
geolocator: ^14.0.1
|
||||
|
||||
nested_scroll_view_plus: ^3.0.0
|
||||
nested_scroll_view_plus: ^3.0.0 #滚动
|
||||
|
||||
ai_barcode_scanner: ^7.0.0
|
||||
city_pickers: ^1.3.0
|
||||
bottom_picker: ^3.2.1
|
||||
|
||||
fluwx: ^5.7.0 #微信sdk
|
||||
flutter_image_compress: ^2.4.0 #处理图片
|
||||
video_thumbnail: ^0.5.6 #视频首帧截取
|
||||
record: ^6.0.0 #音频
|
||||
audioplayers: ^6.5.0 #音频播放
|
||||
flutter_html: ^3.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_launcher_icons: ^0.13.1 # 使用最新版本
|
||||
@ -101,6 +113,8 @@ flutter:
|
||||
|
||||
assets:
|
||||
- assets/images/
|
||||
#notify
|
||||
- assets/images/notify/
|
||||
#logo
|
||||
- assets/images/logo/
|
||||
#avatar
|
||||
|