Przeglądaj źródła

车况界面 半成品

wangmeng 1 miesiąc temu
rodzic
commit
f4030f4bd0
100 zmienionych plików z 5048 dodań i 1091 usunięć
  1. 9 5
      app/build.gradle
  2. 21 0
      app/src/main/AndroidManifest.xml
  3. 6 26
      app/src/main/java/com/narutohuo/xindazhou/MainActivity.kt
  4. 71 3
      app/src/main/java/com/narutohuo/xindazhou/auth/AuthManager.kt
  5. 5 1
      app/src/main/java/com/narutohuo/xindazhou/auth/repository/AuthRepository.kt
  6. 200 22
      app/src/main/java/com/narutohuo/xindazhou/auth/ui/login/LoginActivity.kt
  7. 20 3
      app/src/main/java/com/narutohuo/xindazhou/auth/ui/viewmodel/LoginViewModel.kt
  8. 182 0
      app/src/main/java/com/narutohuo/xindazhou/community/datasource/remote/CommunityApi.kt
  9. 171 0
      app/src/main/java/com/narutohuo/xindazhou/community/datasource/remote/CommunityRemoteDataSource.kt
  10. 14 0
      app/src/main/java/com/narutohuo/xindazhou/community/model/Banner.kt
  11. 21 0
      app/src/main/java/com/narutohuo/xindazhou/community/model/Comment.kt
  12. 10 0
      app/src/main/java/com/narutohuo/xindazhou/community/model/PageResult.kt
  13. 39 0
      app/src/main/java/com/narutohuo/xindazhou/community/model/Post.kt
  14. 14 0
      app/src/main/java/com/narutohuo/xindazhou/community/model/Topic.kt
  15. 100 0
      app/src/main/java/com/narutohuo/xindazhou/community/repository/CommunityRepository.kt
  16. 315 55
      app/src/main/java/com/narutohuo/xindazhou/community/ui/CommunityFragment.kt
  17. 254 0
      app/src/main/java/com/narutohuo/xindazhou/community/ui/adapter/CommentAdapter.kt
  18. 65 0
      app/src/main/java/com/narutohuo/xindazhou/community/ui/adapter/MediaAdapter.kt
  19. 119 0
      app/src/main/java/com/narutohuo/xindazhou/community/ui/adapter/PostAdapter.kt
  20. 78 0
      app/src/main/java/com/narutohuo/xindazhou/community/ui/adapter/TopicAdapter.kt
  21. 328 0
      app/src/main/java/com/narutohuo/xindazhou/community/ui/content/CreatePostActivity.kt
  22. 314 0
      app/src/main/java/com/narutohuo/xindazhou/community/ui/content/PostDetailActivity.kt
  23. 206 0
      app/src/main/java/com/narutohuo/xindazhou/community/ui/dialog/CityPickerDialog.kt
  24. 27 0
      app/src/main/java/com/narutohuo/xindazhou/community/ui/viewmodel/CommentState.kt
  25. 29 0
      app/src/main/java/com/narutohuo/xindazhou/community/ui/viewmodel/CommunityState.kt
  26. 304 0
      app/src/main/java/com/narutohuo/xindazhou/community/ui/viewmodel/CommunityViewModel.kt
  27. 32 0
      app/src/main/java/com/narutohuo/xindazhou/community/ui/viewmodel/CommunityViewModelFactory.kt
  28. 26 0
      app/src/main/java/com/narutohuo/xindazhou/community/ui/viewmodel/CreatePostState.kt
  29. 167 0
      app/src/main/java/com/narutohuo/xindazhou/community/ui/viewmodel/CreatePostViewModel.kt
  30. 276 0
      app/src/main/java/com/narutohuo/xindazhou/community/ui/viewmodel/PostDetailViewModel.kt
  31. 67 0
      app/src/main/java/com/narutohuo/xindazhou/community/util/CityCacheHelper.kt
  32. 105 0
      app/src/main/java/com/narutohuo/xindazhou/community/util/LocationHelper.kt
  33. 11 16
      app/src/main/java/com/narutohuo/xindazhou/launch/AppInitializer.kt
  34. 10 9
      app/src/main/java/com/narutohuo/xindazhou/service/ui/ServiceFragment.kt
  35. 19 15
      app/src/main/java/com/narutohuo/xindazhou/user/ui/UserFragment.kt
  36. 35 0
      app/src/main/java/com/narutohuo/xindazhou/vehicle/datasource/remote/VehicleRemoteDataSource.kt
  37. 69 0
      app/src/main/java/com/narutohuo/xindazhou/vehicle/datasource/remote/VehicleRemoteDataSourceImpl.kt
  38. 97 0
      app/src/main/java/com/narutohuo/xindazhou/vehicle/datasource/remote/http/VehicleApi.kt
  39. 47 0
      app/src/main/java/com/narutohuo/xindazhou/vehicle/model/Vehicle.kt
  40. 52 0
      app/src/main/java/com/narutohuo/xindazhou/vehicle/repository/VehicleRepository.kt
  41. 329 936
      app/src/main/java/com/narutohuo/xindazhou/vehicle/ui/VehicleFragment.kt
  42. 368 0
      app/src/main/java/com/narutohuo/xindazhou/vehicle/ui/binding/VehicleBindActivity.kt
  43. 18 0
      app/src/main/java/com/narutohuo/xindazhou/vehicle/ui/viewmodel/VehicleBindState.kt
  44. 90 0
      app/src/main/java/com/narutohuo/xindazhou/vehicle/ui/viewmodel/VehicleBindViewModel.kt
  45. 26 0
      app/src/main/java/com/narutohuo/xindazhou/vehicle/ui/viewmodel/VehicleBindViewModelFactory.kt
  46. 8 0
      app/src/main/res/color/bottom_nav_item_color_selector.xml
  47. 9 0
      app/src/main/res/color/bottom_nav_item_color_selector_dark.xml
  48. 6 0
      app/src/main/res/drawable/bg_avatar_placeholder.xml
  49. 10 0
      app/src/main/res/drawable/bg_button_outline.xml
  50. 7 0
      app/src/main/res/drawable/bg_button_primary.xml
  51. 19 0
      app/src/main/res/drawable/bg_button_primary_glass.xml
  52. 7 0
      app/src/main/res/drawable/bg_gradient_primary.xml
  53. 8 0
      app/src/main/res/drawable/bg_home_indicator.xml
  54. 8 0
      app/src/main/res/drawable/bg_info_circle.xml
  55. 7 0
      app/src/main/res/drawable/bg_input.xml
  56. 11 0
      app/src/main/res/drawable/bg_location_gradient.xml
  57. 7 0
      app/src/main/res/drawable/bg_notification_badge.xml
  58. 6 0
      app/src/main/res/drawable/bg_tab_selected.xml
  59. 6 0
      app/src/main/res/drawable/bg_tab_unselected.xml
  60. 7 0
      app/src/main/res/drawable/bg_tabbar_dark.xml
  61. 11 0
      app/src/main/res/drawable/bg_tire_pressure_button.xml
  62. 12 0
      app/src/main/res/drawable/bg_vehicle_blur_mask.xml
  63. 8 0
      app/src/main/res/drawable/bg_vehicle_button_active.xml
  64. 11 0
      app/src/main/res/drawable/bg_vehicle_button_glass.xml
  65. 8 0
      app/src/main/res/drawable/bg_vehicle_card_glass.xml
  66. 8 0
      app/src/main/res/drawable/bg_vehicle_card_glass_10dp.xml
  67. 12 0
      app/src/main/res/drawable/bg_vehicle_gradient.xml
  68. BIN
      app/src/main/res/drawable/common_bluetooth.png
  69. 13 0
      app/src/main/res/drawable/common_signal.xml
  70. BIN
      app/src/main/res/drawable/ic_add.png
  71. 11 0
      app/src/main/res/drawable/ic_arrow_right.xml
  72. 11 0
      app/src/main/res/drawable/ic_comment_outline.xml
  73. BIN
      app/src/main/res/drawable/ic_decor_90.png
  74. 11 0
      app/src/main/res/drawable/ic_default_avatar.xml
  75. BIN
      app/src/main/res/drawable/ic_driving_record.png
  76. BIN
      app/src/main/res/drawable/ic_key_share.png
  77. 11 0
      app/src/main/res/drawable/ic_like_outline.xml
  78. BIN
      app/src/main/res/drawable/ic_location_decor.png
  79. BIN
      app/src/main/res/drawable/ic_location_pin.png
  80. BIN
      app/src/main/res/drawable/ic_map_mask.png
  81. BIN
      app/src/main/res/drawable/ic_navigation.png
  82. BIN
      app/src/main/res/drawable/ic_notification.png
  83. BIN
      app/src/main/res/drawable/ic_odo.png
  84. BIN
      app/src/main/res/drawable/ic_power.png
  85. 7 0
      app/src/main/res/drawable/ic_search.xml
  86. BIN
      app/src/main/res/drawable/ic_search_decor.png
  87. 11 0
      app/src/main/res/drawable/ic_share.xml
  88. BIN
      app/src/main/res/drawable/ic_skip.png
  89. BIN
      app/src/main/res/drawable/ic_smart_accessories.png
  90. BIN
      app/src/main/res/drawable/ic_smart_health.png
  91. 11 0
      app/src/main/res/drawable/ic_star_outline.xml
  92. BIN
      app/src/main/res/drawable/ic_store_decor.png
  93. BIN
      app/src/main/res/drawable/ic_tire_normal.png
  94. BIN
      app/src/main/res/drawable/ic_tire_warning.png
  95. BIN
      app/src/main/res/drawable/map_placeholder.png
  96. BIN
      app/src/main/res/drawable/progress_bar_bg.png
  97. BIN
      app/src/main/res/drawable/service_online_support.png
  98. BIN
      app/src/main/res/drawable/service_repair.png
  99. BIN
      app/src/main/res/drawable/service_smart.png
  100. 0 0
      app/src/main/res/drawable/service_store_offline.png

+ 9 - 5
app/build.gradle

@@ -12,7 +12,7 @@ android {
     }
 
     defaultConfig {
-        applicationId "com.mooxygen.user"
+        applicationId "com.dongqingkuaidiandian.chat"
         minSdk 26
         targetSdk 36
         versionCode 1
@@ -38,10 +38,10 @@ android {
     // 签名配置
     signingConfigs {
         release {
-            storeFile file('../jks/Keystore.jks')  // 使用 Keystore.jks
-            storePassword '123456'
-            keyAlias 'keystore'
-            keyPassword '123456'
+            storeFile file('../jks/dongqingkuaidiandian.jks')  // 你的发布用keystore
+            storePassword 'dongqingkuaidiandianq29018'
+            keyAlias 'dongqingkuaidiandianqalias'
+            keyPassword 'dongqingkuaidiandianq29018'
             v1SigningEnabled true
             v2SigningEnabled true
         }
@@ -154,6 +154,7 @@ dependencies {
     implementation 'com.google.android.material:material:1.11.0'
     implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
     implementation 'androidx.cardview:cardview:1.0.0'
+    implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
     
     // ViewModel & LiveData & StateFlow
     implementation libs.androidx.lifecycle.runtime.ktx
@@ -186,6 +187,9 @@ dependencies {
     // ARouter 编译器(用于生成路由代码)
     kapt 'com.alibaba:arouter-compiler:1.5.2'
     
+    // Google Play Services Location (用于定位功能)
+    implementation 'com.google.android.gms:play-services-location:21.0.1'
+    
     // Testing
     testImplementation libs.junit
     androidTestImplementation libs.androidx.junit

+ 21 - 0
app/src/main/AndroidManifest.xml

@@ -45,6 +45,10 @@
         android:maxSdkVersion="32" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" 
         android:maxSdkVersion="32" />
+    <!-- 相机权限(用于拍照和录像) -->
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-feature android:name="android.hardware.camera" android:required="false" />
+    <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
 
     <application
         android:name="com.narutohuo.xindazhou.XinDaZhouApplication"
@@ -86,6 +90,23 @@
             android:exported="false"
             android:theme="@style/Theme.XinDaZhou" />
         
+        <!-- 发帖 Activity -->
+        <activity
+            android:name="com.narutohuo.xindazhou.community.ui.content.CreatePostActivity"
+            android:exported="false"
+            android:theme="@style/Theme.XinDaZhou" />
+        
+        <!-- 车辆绑定 Activity -->
+        <activity
+            android:name="com.narutohuo.xindazhou.vehicle.ui.binding.VehicleBindActivity"
+            android:exported="false"
+            android:theme="@style/Theme.XinDaZhou" />
+        
+        <!-- 帖子详情 Activity -->
+        <activity
+            android:name="com.narutohuo.xindazhou.community.ui.content.PostDetailActivity"
+            android:exported="false"
+            android:theme="@style/Theme.XinDaZhou" />
         
         <!-- LogcatViewer Activity(日志查看器) -->
         <!-- singleTask 确保只有一个实例,多次启动会复用同一个 Activity -->

+ 6 - 26
app/src/main/java/com/narutohuo/xindazhou/MainActivity.kt

@@ -13,7 +13,6 @@ import com.narutohuo.xindazhou.community.ui.CommunityFragment
 import com.narutohuo.xindazhou.service.ui.ServiceFragment
 import com.narutohuo.xindazhou.shop.ui.ShopFragment
 import com.narutohuo.xindazhou.user.ui.UserFragment
-import com.narutohuo.xindazhou.vehicle.ui.PermissionRequestHandler
 import com.narutohuo.xindazhou.vehicle.ui.VehicleFragment
 
 /**
@@ -30,33 +29,14 @@ import com.narutohuo.xindazhou.vehicle.ui.VehicleFragment
  * - 所有业务模块都是Fragment,通过FragmentTransaction切换
  * - 默认显示VehicleFragment
  */
-class MainActivity : BaseActivity<ActivityMainBinding>(), PermissionRequestHandler {
+class MainActivity : BaseActivity<ActivityMainBinding>() {
     
     private val TAG = "MainActivity"
     
-    // 权限请求回调
-    private var permissionCallback: ((Map<String, Boolean>) -> Unit)? = null
-    
-    // 权限请求器
-    private val permissionLauncher = registerForActivityResult(
-        ActivityResultContracts.RequestMultiplePermissions()
-    ) { permissions ->
-        permissionCallback?.invoke(permissions)
-        permissionCallback = null
-    }
-    
     override fun getViewBinding(): ActivityMainBinding {
         return ActivityMainBinding.inflate(layoutInflater)
     }
     
-    override fun requestPermissions(
-        permissions: Array<String>,
-        callback: (Map<String, Boolean>) -> Unit
-    ) {
-        permissionCallback = callback
-        permissionLauncher.launch(permissions)
-    }
-    
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         
@@ -112,14 +92,14 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), PermissionRequestHandl
                     switchFragment(CommunityFragment())
                     true
                 }
-                R.id.serviceFragment -> {
-                    switchFragment(ServiceFragment())
-                    true
-                }
                 R.id.shopFragment -> {
                     switchFragment(ShopFragment())
                     true
                 }
+                R.id.serviceFragment -> {
+                    switchFragment(ServiceFragment())
+                    true
+                }
                 R.id.userFragment -> {
                     switchFragment(UserFragment())
                     true
@@ -169,8 +149,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), PermissionRequestHandl
         when (currentFragment) {
             is VehicleFragment -> binding.bottomNavView.selectedItemId = R.id.vehicleFragment
             is CommunityFragment -> binding.bottomNavView.selectedItemId = R.id.communityFragment
-            is ServiceFragment -> binding.bottomNavView.selectedItemId = R.id.serviceFragment
             is ShopFragment -> binding.bottomNavView.selectedItemId = R.id.shopFragment
+            is ServiceFragment -> binding.bottomNavView.selectedItemId = R.id.serviceFragment
             is UserFragment -> binding.bottomNavView.selectedItemId = R.id.userFragment
         }
     }

+ 71 - 3
app/src/main/java/com/narutohuo/xindazhou/auth/AuthManager.kt

@@ -1,6 +1,10 @@
 package com.narutohuo.xindazhou.auth
 
+import android.app.Application
 import android.content.Context
+import android.content.Intent
+import android.os.Handler
+import android.os.Looper
 import androidx.fragment.app.FragmentActivity
 import androidx.lifecycle.lifecycleScope
 import com.narutohuo.xindazhou.auth.datasource.remote.AuthApi
@@ -8,10 +12,13 @@ import com.narutohuo.xindazhou.auth.model.LoginRequest
 import com.narutohuo.xindazhou.auth.model.LoginResponse
 import com.narutohuo.xindazhou.auth.model.RegisterRequest
 import com.narutohuo.xindazhou.auth.storage.TokenStore
+import com.narutohuo.xindazhou.auth.ui.login.LoginActivity
 import com.narutohuo.xindazhou.common.network.ApiManager
 import com.narutohuo.xindazhou.common.network.ApiBaseRemoteDataSource
 import com.narutohuo.xindazhou.common.network.NetworkManager
+import com.narutohuo.xindazhou.common.ui.ActivityManager
 import com.narutohuo.xindazhou.core.log.ILog
+import com.narutohuo.xindazhou.socketio.SocketIOManager
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
@@ -62,26 +69,87 @@ object AuthManager : ApiBaseRemoteDataSource() {
     }
     
     private var applicationContext: Context? = null
+    private var application: Application? = null
     private var isInitialized = false
     
     /**
      * 设置应用上下文(懒加载模式)
+     * 
+     * 统一配置所有认证相关的模块:
+     * - Token 存储初始化
+     * - Network 模块的 Token Provider、刷新 Provider 和刷新失败回调
+     * - SocketIO 模块的 Token 刷新 Provider 和登录状态检查 Provider
+     * 
+     * @param context Application 或 Context 实例
      */
     fun setContext(context: Context) {
         this.applicationContext = context.applicationContext
+        this.application = context as? Application
+        
+        // 初始化 Token 存储
         TokenStore.init(context.applicationContext)
         
-        // 配置 Network 模块的 Token Provider(自动添加 Token 到请求头)
+        // ========== Network 模块配置 ==========
+        
+        // 1. 配置 Token Provider(自动添加 Token 到请求头)
         ApiManager.setTokenProvider {
             TokenStore.getAccessToken()
         }
         
-        // 配置 Network 模块的 Token 刷新 Provider(自动刷新过期的 Token
+        // 2. 配置 Token 刷新 Provider(HTTP 401/402 时自动刷新)
         NetworkManager.refreshTokenProvider = {
             refreshTokenIfNeeded()
         }
         
-        ILog.d(TAG, "AuthManager context 已设置,Network 模块已配置")
+        // 3. 配置 Token 刷新失败回调(刷新失败时跳转登录界面)
+        NetworkManager.onTokenRefreshFailed = {
+            Handler(Looper.getMainLooper()).post {
+                navigateToLogin()
+            }
+        }
+        
+        // ========== SocketIO 模块配置 ==========
+        
+        // 4. 配置 Token 刷新 Provider(Socket 连接时刷新 Token)
+        SocketIOManager.refreshTokenProvider = {
+            refreshTokenIfNeeded()
+        }
+        
+        // 5. 配置登录状态检查 Provider(检查用户是否已登录)
+        SocketIOManager.isLoggedInProvider = {
+            isLoggedIn()
+        }
+        
+        ILog.d(TAG, "AuthManager context 已设置,所有认证相关模块已配置")
+    }
+    
+    /**
+     * 跳转到登录界面
+     * 
+     * 当 Token 刷新失败时自动调用
+     */
+    private fun navigateToLogin() {
+        try {
+            val currentActivity = ActivityManager.getCurrentActivity()
+            if (currentActivity != null) {
+                val intent = Intent(currentActivity, LoginActivity::class.java)
+                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
+                currentActivity.startActivity(intent)
+                ILog.d(TAG, "Token 刷新失败,已跳转到登录界面")
+            } else {
+                val context = applicationContext ?: application
+                if (context != null) {
+                    val intent = Intent(context, LoginActivity::class.java)
+                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                    context.startActivity(intent)
+                    ILog.d(TAG, "Token 刷新失败,已跳转到登录界面(使用 Application Context)")
+                } else {
+                    ILog.w(TAG, "无法跳转登录界面:没有可用的 Context")
+                }
+            }
+        } catch (e: Exception) {
+            ILog.e(TAG, "跳转登录界面失败", e)
+        }
     }
     
     /**

+ 5 - 1
app/src/main/java/com/narutohuo/xindazhou/auth/repository/AuthRepository.kt

@@ -52,16 +52,20 @@ class AuthRepository(
      * @return 登录结果(包含 Token、用户信息等)
      */
     suspend fun login(mobile: String, password: String): Result<LoginResponse> {
+        ILog.d(TAG, "开始登录,手机号: $mobile")
+        
         // 步骤1:转换为请求对象(Retrofit 需要对象,不能直接用多个参数)
         val request = LoginRequest(mobile, password)
         
         // 步骤2:调用远程数据源(它使用 Network 模块发起请求)
         val result = remoteDataSource.login(request)
         
-        // 步骤3:如果成功,保存 Token 到本地
+        // 步骤3:处理结果
         result.onSuccess { response ->
             localDataSource.saveToken(response)
             ILog.d(TAG, "登录成功,Token已保存")
+        }.onFailure { error ->
+            ILog.e(TAG, "登录失败: ${error.message}", error)
         }
         
         // 步骤4:返回结果

+ 200 - 22
app/src/main/java/com/narutohuo/xindazhou/auth/ui/login/LoginActivity.kt

@@ -11,6 +11,7 @@ import com.google.android.material.textfield.TextInputEditText
 import com.narutohuo.xindazhou.R
 import com.narutohuo.xindazhou.common.dialog.ServerConfigDialog
 import com.narutohuo.xindazhou.common.ui.BaseActivity
+import com.narutohuo.xindazhou.common.ui.GlassmorphismAnimator
 import com.narutohuo.xindazhou.databinding.ActivityLoginBinding
 import com.narutohuo.xindazhou.socketio.SocketIOManager
 import com.narutohuo.xindazhou.auth.ui.viewmodel.LoginViewModel
@@ -27,12 +28,30 @@ import kotlinx.coroutines.launch
 class LoginActivity : BaseActivity<ActivityLoginBinding>() {
     
     private lateinit var viewModel: LoginViewModel
-    private lateinit var etMobile: TextInputEditText
+    
+    // 登录类型切换
+    private var isVerifyCodeMode = true
+    
+    // 验证码登录相关
+    private lateinit var etMobileVerify: TextInputEditText
+    private lateinit var etVerifyCode: TextInputEditText
+    private lateinit var btnGetVerifyCode: MaterialButton
+    private lateinit var btnLoginVerify: MaterialButton
+    
+    // 密码登录相关
+    private lateinit var etMobilePassword: TextInputEditText
     private lateinit var etPassword: TextInputEditText
-    private lateinit var btnLogin: MaterialButton
-    private lateinit var btnRegister: MaterialButton
+    private lateinit var cbRememberPassword: android.widget.CheckBox
+    private lateinit var btnLoginPassword: MaterialButton
+    
+    // 通用
+    private lateinit var tvVerifyCodeTab: android.widget.TextView
+    private lateinit var tvPasswordTab: android.widget.TextView
+    private lateinit var llVerifyCodeForm: android.view.View
+    private lateinit var llPasswordForm: android.view.View
+    private lateinit var tvRegister: android.widget.TextView
     private lateinit var progressBar: View
-    private var ivServerConfig: View? = null
+    private var cardServerConfig: View? = null
     
     override fun getViewBinding(): ActivityLoginBinding {
         return ActivityLoginBinding.inflate(layoutInflater)
@@ -52,32 +71,128 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>() {
     }
     
     override fun initView() {
-        etMobile = binding.etMobile
+        // 登录类型切换
+        tvVerifyCodeTab = binding.tvVerifyCodeTab
+        tvPasswordTab = binding.tvPasswordTab
+        llVerifyCodeForm = binding.llVerifyCodeForm
+        llPasswordForm = binding.llPasswordForm
+        
+        // 验证码登录
+        etMobileVerify = binding.etMobileVerify
+        etVerifyCode = binding.etVerifyCode
+        btnGetVerifyCode = binding.btnGetVerifyCode
+        btnLoginVerify = binding.btnLoginVerify
+        
+        // 密码登录
+        etMobilePassword = binding.etMobilePassword
         etPassword = binding.etPassword
-        btnLogin = binding.btnLogin
-        btnRegister = binding.btnRegister
+        cbRememberPassword = binding.cbRememberPassword
+        btnLoginPassword = binding.btnLoginPassword
+        
+        // 通用
+        tvRegister = binding.tvRegister
         progressBar = binding.progressBar
-        ivServerConfig = binding.ivServerConfig
+        cardServerConfig = binding.cardServerConfig
         
-        // 【测试功能】服务器配置按钮
-        ivServerConfig?.setOnClickListener {
-            val dialog = ServerConfigDialog()
-            dialog.show(supportFragmentManager, "ServerConfigDialog")
+        // 登录类型切换
+        tvVerifyCodeTab.setOnClickListener {
+            switchToVerifyCodeMode()
+        }
+        tvPasswordTab.setOnClickListener {
+            switchToPasswordMode()
+        }
+        
+        // 验证码登录按钮
+        btnGetVerifyCode.setOnClickListener {
+            val mobile = etMobileVerify.text?.toString()?.trim() ?: ""
+            if (mobile.isEmpty()) {
+                Toast.makeText(this, getString(R.string.mobile_empty), Toast.LENGTH_SHORT).show()
+                return@setOnClickListener
+            }
+            // TODO: 实现获取验证码逻辑
+            Toast.makeText(this, "验证码已发送", Toast.LENGTH_SHORT).show()
         }
         
-        // 登录按钮
-        btnLogin.setOnClickListener {
-            val mobile = etMobile.text?.toString()?.trim() ?: ""
+        btnLoginVerify.setOnClickListener {
+            val mobile = etMobileVerify.text?.toString()?.trim() ?: ""
+            val verifyCode = etVerifyCode.text?.toString()?.trim() ?: ""
+            if (mobile.isEmpty()) {
+                Toast.makeText(this, getString(R.string.mobile_empty), Toast.LENGTH_SHORT).show()
+                return@setOnClickListener
+            }
+            if (verifyCode.isEmpty()) {
+                Toast.makeText(this, getString(R.string.verify_code_empty), Toast.LENGTH_SHORT).show()
+                return@setOnClickListener
+            }
+            // TODO: 实现验证码登录逻辑
+            Toast.makeText(this, "验证码登录功能待实现", Toast.LENGTH_SHORT).show()
+        }
+        
+        // 密码登录按钮
+        btnLoginPassword.setOnClickListener {
+            val mobile = etMobilePassword.text?.toString()?.trim() ?: ""
             val password = etPassword.text?.toString() ?: ""
-            
-            // 调用 ViewModel 方法(简单!)
+            if (mobile.isEmpty()) {
+                Toast.makeText(this, getString(R.string.mobile_empty), Toast.LENGTH_SHORT).show()
+                return@setOnClickListener
+            }
+            if (password.isEmpty()) {
+                Toast.makeText(this, getString(R.string.password_empty), Toast.LENGTH_SHORT).show()
+                return@setOnClickListener
+            }
+            // 调用 ViewModel 方法
             viewModel.login(mobile, password)
         }
         
-        // 注册按钮
-        btnRegister.setOnClickListener {
+        // 注册链接
+        tvRegister.setOnClickListener {
             navigateToRegister()
         }
+        
+        // 添加玻璃拟态动画效果
+        setupGlassmorphismEffects()
+        
+        // 忘记密码
+        val tvForgotPassword = binding.root.findViewById<android.widget.TextView>(R.id.tvForgotPassword)
+        tvForgotPassword?.setOnClickListener {
+            // TODO: 实现忘记密码功能
+            Toast.makeText(this, "忘记密码功能待实现", Toast.LENGTH_SHORT).show()
+        }
+        
+        // 【服务器配置按钮】设置网络地址
+        cardServerConfig?.setOnClickListener {
+            val dialog = ServerConfigDialog()
+            dialog.show(supportFragmentManager, "ServerConfigDialog")
+        }
+        
+        // 默认显示验证码登录
+        switchToVerifyCodeMode()
+    }
+    
+    /**
+     * 切换到验证码登录模式
+     */
+    private fun switchToVerifyCodeMode() {
+        isVerifyCodeMode = true
+        tvVerifyCodeTab.setTextColor(getColor(R.color.accent_primary))
+        tvVerifyCodeTab.setTypeface(null, android.graphics.Typeface.BOLD)
+        tvPasswordTab.setTextColor(getColor(R.color.text_tertiary))
+        tvPasswordTab.setTypeface(null, android.graphics.Typeface.NORMAL)
+        llVerifyCodeForm.visibility = android.view.View.VISIBLE
+        llPasswordForm.visibility = android.view.View.GONE
+    }
+    
+    /**
+     * 切换到密码登录模式
+     */
+    private fun switchToPasswordMode() {
+        isVerifyCodeMode = false
+        tvPasswordTab.setTextColor(getColor(R.color.accent_primary))
+        tvPasswordTab.setTypeface(null, android.graphics.Typeface.BOLD)
+        tvVerifyCodeTab.setTextColor(getColor(R.color.text_tertiary))
+        tvVerifyCodeTab.setTypeface(null, android.graphics.Typeface.NORMAL)
+        llPasswordForm.visibility = android.view.View.VISIBLE
+        llVerifyCodeForm.visibility = android.view.View.GONE
     }
     
     /**
@@ -93,12 +208,14 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>() {
                     is LoginState.Loading -> {
                         // 显示加载状态
                         progressBar.visibility = View.VISIBLE
-                        btnLogin.isEnabled = false
+                        btnLoginPassword.isEnabled = false
+                        btnLoginVerify.isEnabled = false
                     }
                     is LoginState.Success -> {
                         // 登录成功
                         progressBar.visibility = View.GONE
-                        btnLogin.isEnabled = true
+                        btnLoginPassword.isEnabled = true
+                        btnLoginVerify.isEnabled = true
                         Toast.makeText(this@LoginActivity, state.message, Toast.LENGTH_SHORT).show()
                         // 登录成功后,确保 SocketIO 已连接
                         SocketIOManager.ensureConnected()
@@ -110,7 +227,8 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>() {
                     is LoginState.Error -> {
                         // 登录失败
                         progressBar.visibility = View.GONE
-                        btnLogin.isEnabled = true
+                        btnLoginPassword.isEnabled = true
+                        btnLoginVerify.isEnabled = true
                         Toast.makeText(this@LoginActivity, state.message, Toast.LENGTH_SHORT).show()
                     }
                 }
@@ -118,6 +236,66 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>() {
         }
     }
     
+    // ==================== 玻璃拟态动画效果 ====================
+    
+    /**
+     * 设置玻璃拟态动画效果
+     * 为卡片和按钮添加呼吸感和悬浮感
+     */
+    private fun setupGlassmorphismEffects() {
+        // 为登录表单卡片添加呼吸感和悬浮感
+        binding.cardLoginForm?.let { card ->
+            GlassmorphismAnimator.addGlassmorphismEffects(
+                view = card,
+                breathingDuration = 2500L, // 2.5秒周期
+                breathingAlphaMin = 0.9f,   // 最小透明度 0.9
+                breathingAlphaMax = 1.0f,   // 最大透明度 1.0
+                floatingScaleMin = 0.99f,   // 按下时缩小到 0.99
+                floatingScaleMax = 1.0f,    // 正常大小 1.0
+                floatingElevationMin = 4f,  // 最小 elevation 4dp
+                floatingElevationMax = 12f  // 最大 elevation 12dp
+            )
+        }
+        
+        // 为登录按钮添加悬浮感(不添加呼吸感,避免过于抢眼)
+        btnLoginVerify.setOnTouchListener { view, event ->
+            handleButtonFloatingEffect(view, event)
+        }
+        btnLoginPassword.setOnTouchListener { view, event ->
+            handleButtonFloatingEffect(view, event)
+        }
+    }
+    
+    /**
+     * 处理按钮的悬浮感效果
+     */
+    private fun handleButtonFloatingEffect(view: View, event: android.view.MotionEvent): Boolean {
+        when (event.action) {
+            android.view.MotionEvent.ACTION_DOWN -> {
+                // 按下时缩小并提升 elevation
+                view.animate()
+                    .scaleX(0.97f)
+                    .scaleY(0.97f)
+                    .translationZ(8f)
+                    .setDuration(150)
+                    .setInterpolator(android.view.animation.AccelerateDecelerateInterpolator())
+                    .start()
+            }
+            android.view.MotionEvent.ACTION_UP,
+            android.view.MotionEvent.ACTION_CANCEL -> {
+                // 释放时恢复
+                view.animate()
+                    .scaleX(1.0f)
+                    .scaleY(1.0f)
+                    .translationZ(0f)
+                    .setDuration(200)
+                    .setInterpolator(android.view.animation.AccelerateDecelerateInterpolator())
+                    .start()
+            }
+        }
+        return false // 不消费事件,让按钮的点击事件正常触发
+    }
+    
     // ==================== 导航方法 ====================
     
     private fun navigateToMain() {

+ 20 - 3
app/src/main/java/com/narutohuo/xindazhou/auth/ui/viewmodel/LoginViewModel.kt

@@ -4,6 +4,7 @@ import android.app.Application
 import androidx.lifecycle.viewModelScope
 import com.narutohuo.xindazhou.auth.repository.AuthRepository
 import com.narutohuo.xindazhou.common.ui.BaseViewModel
+import com.narutohuo.xindazhou.core.log.ILog
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.launch
@@ -35,25 +36,41 @@ class LoginViewModel(
      * @param password 密码
      */
     fun login(mobile: String, password: String) {
+        ILog.d(tag, "开始登录流程,手机号: $mobile")
+        
         // 输入验证
         if (mobile.isBlank()) {
+            ILog.w(tag, "登录失败:手机号为空")
             _loginState.value = LoginState.Error("手机号不能为空")
             return
         }
         
         if (password.length < MIN_PASSWORD_LENGTH || password.length > MAX_PASSWORD_LENGTH) {
+            ILog.w(tag, "登录失败:密码长度不符合要求,当前长度: ${password.length}")
             _loginState.value = LoginState.Error("密码长度应为${MIN_PASSWORD_LENGTH}-${MAX_PASSWORD_LENGTH}位")
             return
         }
         
+        ILog.d(tag, "输入验证通过,开始调用登录接口")
+        
         // 使用 BaseViewModel 提供的 executeRequest 方法(自动处理 Loading/Success/Error)
         executeRequest(
             stateFlow = _loginState,
-            onLoading = { LoginState.Loading },
-            onSuccess = { LoginState.Success(MSG_LOGIN_SUCCESS) },
-            onError = { msg -> LoginState.Error(msg) },
+            onLoading = { 
+                ILog.d(tag, "登录请求发送中...")
+                LoginState.Loading 
+            },
+            onSuccess = { 
+                ILog.d(tag, "登录成功")
+                LoginState.Success(MSG_LOGIN_SUCCESS) 
+            },
+            onError = { msg -> 
+                ILog.e(tag, "登录失败: $msg")
+                LoginState.Error(msg) 
+            },
             errorMessage = MSG_LOGIN_FAILED
         ) {
+            ILog.d(tag, "调用 authRepository.login()")
             authRepository.login(mobile, password)
         }
     }

+ 182 - 0
app/src/main/java/com/narutohuo/xindazhou/community/datasource/remote/CommunityApi.kt

@@ -0,0 +1,182 @@
+package com.narutohuo.xindazhou.community.datasource.remote
+
+import com.narutohuo.xindazhou.community.model.Banner
+import com.narutohuo.xindazhou.community.model.Comment
+import com.narutohuo.xindazhou.community.model.PageResult
+import com.narutohuo.xindazhou.community.model.Post
+import com.narutohuo.xindazhou.community.model.Topic
+import com.narutohuo.xindazhou.common.network.ApiCommonResult
+import retrofit2.Response
+import retrofit2.http.*
+
+/**
+ * 社区相关API接口
+ */
+interface CommunityApi {
+    
+    // ========== 帖子相关 ==========
+    
+    /**
+     * 获得帖子分页列表
+     */
+    @GET("community/post/page")
+    suspend fun getPostPage(
+        @Query("pageNo") pageNo: Int,
+        @Query("pageSize") pageSize: Int,
+        @Query("category") category: String? = null, // official/hot/latest/recommended
+        @Query("keyword") keyword: String? = null,
+        @Query("sort") sort: String? = null, // time/popularity
+        @Query("city") city: String? = null,
+        @Query("topicId") topicId: Long? = null
+    ): Response<ApiCommonResult<PageResult<Post>>>
+    
+    /**
+     * 获得帖子详情
+     */
+    @GET("community/post/get")
+    suspend fun getPost(
+        @Query("id") id: Long
+    ): Response<ApiCommonResult<Post>>
+    
+    /**
+     * 创建帖子
+     */
+    @POST("community/post/create")
+    suspend fun createPost(
+        @Body request: CreatePostRequest
+    ): Response<ApiCommonResult<Long>>
+    
+    /**
+     * 更新帖子
+     */
+    @PUT("community/post/update")
+    suspend fun updatePost(
+        @Body request: UpdatePostRequest
+    ): Response<ApiCommonResult<Boolean>>
+    
+    /**
+     * 删除帖子
+     */
+    @DELETE("community/post/delete")
+    suspend fun deletePost(
+        @Query("id") id: Long
+    ): Response<ApiCommonResult<Boolean>>
+    
+    // ========== 评论相关 ==========
+    
+    /**
+     * 获得评论分页列表
+     */
+    @GET("community/comment/page")
+    suspend fun getCommentPage(
+        @Query("pageNo") pageNo: Int,
+        @Query("pageSize") pageSize: Int,
+        @Query("postId") postId: Long
+    ): Response<ApiCommonResult<PageResult<Comment>>>
+    
+    /**
+     * 获得帖子的评论树
+     */
+    @GET("community/comment/tree")
+    suspend fun getCommentTree(
+        @Query("postId") postId: Long
+    ): Response<ApiCommonResult<List<Comment>>>
+    
+    /**
+     * 创建评论
+     */
+    @POST("community/comment/create")
+    suspend fun createComment(
+        @Body request: CreateCommentRequest
+    ): Response<ApiCommonResult<Long>>
+    
+    /**
+     * 删除评论
+     */
+    @DELETE("community/comment/delete")
+    suspend fun deleteComment(
+        @Query("id") id: Long
+    ): Response<ApiCommonResult<Boolean>>
+    
+    // ========== 互动相关 ==========
+    
+    /**
+     * 点赞/取消点赞
+     */
+    @POST("community/like/toggle")
+    suspend fun toggleLike(
+        @Body request: LikeRequest
+    ): Response<ApiCommonResult<Boolean>>
+    
+    /**
+     * 收藏/取消收藏
+     */
+    @POST("community/collect/toggle")
+    suspend fun toggleCollect(
+        @Query("postId") postId: Long
+    ): Response<ApiCommonResult<Boolean>>
+    
+    /**
+     * 关注/取消关注
+     */
+    @POST("community/follow/toggle")
+    suspend fun toggleFollow(
+        @Query("followUserId") followUserId: Long
+    ): Response<ApiCommonResult<Boolean>>
+    
+    // ========== Banner相关 ==========
+    
+    /**
+     * 获得Banner列表
+     */
+    @GET("community/banner/list")
+    suspend fun getBannerList(): Response<ApiCommonResult<List<Banner>>>
+    
+    // ========== 话题相关 ==========
+    
+    /**
+     * 获得启用的话题列表
+     */
+    @GET("community/topic/listEnabled")
+    suspend fun getTopicList(): Response<ApiCommonResult<List<Topic>>>
+    
+    // ========== 请求模型 ==========
+    
+    data class CreatePostRequest(
+        val title: String,
+        val content: String?,
+        val contentType: Int,
+        val mediaList: List<PostMediaRequest>?,
+        val tags: String?
+    )
+    
+    data class PostMediaRequest(
+        val mediaType: Int,
+        val mediaUrl: String,
+        val coverUrl: String?,
+        val duration: Int?,
+        val fileSize: Long?,
+        val sort: Int?
+    )
+    
+    data class UpdatePostRequest(
+        val id: Long,
+        val title: String,
+        val content: String?,
+        val contentType: Int,
+        val mediaList: List<PostMediaRequest>?,
+        val tags: String?
+    )
+    
+    data class CreateCommentRequest(
+        val postId: Long,
+        val parentId: Long?,
+        val content: String
+    )
+    
+    data class LikeRequest(
+        val targetType: Int, // 1-帖子,2-评论
+        val targetId: Long
+    )
+}
+

+ 171 - 0
app/src/main/java/com/narutohuo/xindazhou/community/datasource/remote/CommunityRemoteDataSource.kt

@@ -0,0 +1,171 @@
+package com.narutohuo.xindazhou.community.datasource.remote
+
+import com.narutohuo.xindazhou.community.datasource.remote.CommunityApi.CreateCommentRequest
+import com.narutohuo.xindazhou.community.datasource.remote.CommunityApi.CreatePostRequest
+import com.narutohuo.xindazhou.community.datasource.remote.CommunityApi.LikeRequest
+import com.narutohuo.xindazhou.community.datasource.remote.CommunityApi.UpdatePostRequest
+import com.narutohuo.xindazhou.community.model.Banner
+import com.narutohuo.xindazhou.community.model.Comment
+import com.narutohuo.xindazhou.community.model.PageResult
+import com.narutohuo.xindazhou.community.model.Post
+import com.narutohuo.xindazhou.community.model.Topic
+import com.narutohuo.xindazhou.common.network.ApiBaseRemoteDataSource
+import com.narutohuo.xindazhou.common.network.ApiManager
+import com.narutohuo.xindazhou.common.network.response.ApiResponse
+
+/**
+ * 社区远程数据源接口
+ */
+interface CommunityRemoteDataSource {
+    // ========== 帖子相关 ==========
+    suspend fun getPostPage(
+        pageNo: Int,
+        pageSize: Int,
+        category: String? = null,
+        keyword: String? = null,
+        sort: String? = null,
+        city: String? = null,
+        topicId: Long? = null
+    ): ApiResponse<PageResult<Post>>
+    
+    suspend fun getPost(id: Long): ApiResponse<Post>
+    
+    suspend fun createPost(request: CreatePostRequest): ApiResponse<Long>
+    
+    suspend fun updatePost(request: UpdatePostRequest): ApiResponse<Boolean>
+    
+    suspend fun deletePost(id: Long): ApiResponse<Boolean>
+    
+    // ========== 评论相关 ==========
+    suspend fun getCommentPage(
+        pageNo: Int,
+        pageSize: Int,
+        postId: Long
+    ): ApiResponse<PageResult<Comment>>
+    
+    suspend fun getCommentTree(postId: Long): ApiResponse<List<Comment>>
+    
+    suspend fun createComment(request: CreateCommentRequest): ApiResponse<Long>
+    
+    suspend fun deleteComment(id: Long): ApiResponse<Boolean>
+    
+    // ========== 互动相关 ==========
+    suspend fun toggleLike(request: LikeRequest): ApiResponse<Boolean>
+    
+    suspend fun toggleCollect(postId: Long): ApiResponse<Boolean>
+    
+    suspend fun toggleFollow(followUserId: Long): ApiResponse<Boolean>
+    
+    // ========== Banner相关 ==========
+    suspend fun getBannerList(): ApiResponse<List<Banner>>
+    
+    // ========== 话题相关 ==========
+    suspend fun getTopicList(): ApiResponse<List<Topic>>
+}
+
+/**
+ * 社区远程数据源实现
+ */
+class CommunityRemoteDataSourceImpl : ApiBaseRemoteDataSource(), CommunityRemoteDataSource {
+    
+    private val communityApi: CommunityApi by lazy {
+        ApiManager.create<CommunityApi>()
+    }
+    
+    override suspend fun getPostPage(
+        pageNo: Int,
+        pageSize: Int,
+        category: String?,
+        keyword: String?,
+        sort: String?,
+        city: String?,
+        topicId: Long?
+    ): ApiResponse<PageResult<Post>> {
+        return executeRequestResponse(
+            request = { communityApi.getPostPage(pageNo, pageSize, category, keyword, sort, city, topicId) }
+        )
+    }
+    
+    override suspend fun getPost(id: Long): ApiResponse<Post> {
+        return executeRequestResponse(
+            request = { communityApi.getPost(id) }
+        )
+    }
+    
+    override suspend fun createPost(request: CreatePostRequest): ApiResponse<Long> {
+        return executeRequestResponse(
+            request = { communityApi.createPost(request) }
+        )
+    }
+    
+    override suspend fun updatePost(request: UpdatePostRequest): ApiResponse<Boolean> {
+        return executeRequestResponse(
+            request = { communityApi.updatePost(request) }
+        )
+    }
+    
+    override suspend fun deletePost(id: Long): ApiResponse<Boolean> {
+        return executeRequestResponse(
+            request = { communityApi.deletePost(id) }
+        )
+    }
+    
+    override suspend fun getCommentPage(
+        pageNo: Int,
+        pageSize: Int,
+        postId: Long
+    ): ApiResponse<PageResult<Comment>> {
+        return executeRequestResponse(
+            request = { communityApi.getCommentPage(pageNo, pageSize, postId) }
+        )
+    }
+    
+    override suspend fun getCommentTree(postId: Long): ApiResponse<List<Comment>> {
+        return executeRequestResponse(
+            request = { communityApi.getCommentTree(postId) }
+        )
+    }
+    
+    override suspend fun createComment(request: CreateCommentRequest): ApiResponse<Long> {
+        return executeRequestResponse(
+            request = { communityApi.createComment(request) }
+        )
+    }
+    
+    override suspend fun deleteComment(id: Long): ApiResponse<Boolean> {
+        return executeRequestResponse(
+            request = { communityApi.deleteComment(id) }
+        )
+    }
+    
+    override suspend fun toggleLike(request: LikeRequest): ApiResponse<Boolean> {
+        return executeRequestResponse(
+            request = { communityApi.toggleLike(request) }
+        )
+    }
+    
+    override suspend fun toggleCollect(postId: Long): ApiResponse<Boolean> {
+        return executeRequestResponse(
+            request = { communityApi.toggleCollect(postId) }
+        )
+    }
+    
+    override suspend fun toggleFollow(followUserId: Long): ApiResponse<Boolean> {
+        return executeRequestResponse(
+            request = { communityApi.toggleFollow(followUserId) }
+        )
+    }
+    
+    override suspend fun getBannerList(): ApiResponse<List<Banner>> {
+        return executeRequestResponse(
+            request = { communityApi.getBannerList() }
+        )
+    }
+    
+    override suspend fun getTopicList(): ApiResponse<List<Topic>> {
+        return executeRequestResponse(
+            request = { communityApi.getTopicList() }
+        )
+    }
+}
+

+ 14 - 0
app/src/main/java/com/narutohuo/xindazhou/community/model/Banner.kt

@@ -0,0 +1,14 @@
+package com.narutohuo.xindazhou.community.model
+
+/**
+ * Banner数据模型
+ */
+data class Banner(
+    val id: Long,
+    val imageUrl: String,
+    val description: String?,
+    val pushType: Int, // 1-社区文章,2-社区活动,3-外部链接
+    val pushTarget: String?,
+    val sort: Int
+)
+

+ 21 - 0
app/src/main/java/com/narutohuo/xindazhou/community/model/Comment.kt

@@ -0,0 +1,21 @@
+package com.narutohuo.xindazhou.community.model
+
+/**
+ * 评论数据模型
+ */
+data class Comment(
+    val id: Long,
+    val postId: Long,
+    val userId: Long,
+    val userNickname: String?,
+    val userAvatar: String?,
+    val parentId: Long, // 0表示顶级评论
+    val parentUserNickname: String?,
+    val content: String,
+    val likeCount: Int,
+    val status: Int, // 1-正常,2-删除
+    val isLiked: Boolean?,
+    val replies: List<Comment>?,
+    val createTime: Long? // 时间戳(毫秒)
+)
+

+ 10 - 0
app/src/main/java/com/narutohuo/xindazhou/community/model/PageResult.kt

@@ -0,0 +1,10 @@
+package com.narutohuo.xindazhou.community.model
+
+/**
+ * 分页结果
+ */
+data class PageResult<T>(
+    val list: List<T>,
+    val total: Long
+)
+

+ 39 - 0
app/src/main/java/com/narutohuo/xindazhou/community/model/Post.kt

@@ -0,0 +1,39 @@
+package com.narutohuo.xindazhou.community.model
+
+/**
+ * 帖子数据模型
+ */
+data class Post(
+    val id: Long,
+    val userId: Long,
+    val userNickname: String?,
+    val userAvatar: String?,
+    val title: String,
+    val content: String?,
+    val contentType: Int, // 1-纯文本,2-图片,3-视频,4-图文混合
+    val mediaList: List<PostMedia>?,
+    val viewCount: Int,
+    val likeCount: Int,
+    val commentCount: Int,
+    val collectCount: Int,
+    val status: Int, // 1-正常,2-删除,3-审核中
+    val isTop: Int, // 0-否,1-是
+    val isEssence: Int, // 0-否,1-是
+    val tags: String?,
+    val isLiked: Boolean?,
+    val isCollected: Boolean?,
+    val isFollowed: Boolean?,
+    val createTime: Long? // 时间戳(毫秒)
+)
+
+/**
+ * 帖子媒体
+ */
+data class PostMedia(
+    val mediaType: Int, // 1-图片,2-视频
+    val mediaUrl: String,
+    val coverUrl: String?,
+    val duration: Int?,
+    val fileSize: Long?
+)
+

+ 14 - 0
app/src/main/java/com/narutohuo/xindazhou/community/model/Topic.kt

@@ -0,0 +1,14 @@
+package com.narutohuo.xindazhou.community.model
+
+/**
+ * 话题数据模型
+ */
+data class Topic(
+    val id: Long,
+    val name: String,
+    val description: String?,
+    val postCount: Int,
+    val sort: Int,
+    val status: Int
+)
+

+ 100 - 0
app/src/main/java/com/narutohuo/xindazhou/community/repository/CommunityRepository.kt

@@ -0,0 +1,100 @@
+package com.narutohuo.xindazhou.community.repository
+
+import com.narutohuo.xindazhou.community.datasource.remote.CommunityApi.CreateCommentRequest
+import com.narutohuo.xindazhou.community.datasource.remote.CommunityApi.CreatePostRequest
+import com.narutohuo.xindazhou.community.datasource.remote.CommunityApi.LikeRequest
+import com.narutohuo.xindazhou.community.datasource.remote.CommunityApi.UpdatePostRequest
+import com.narutohuo.xindazhou.community.datasource.remote.CommunityRemoteDataSource
+import com.narutohuo.xindazhou.community.model.Banner
+import com.narutohuo.xindazhou.community.model.Comment
+import com.narutohuo.xindazhou.community.model.PageResult
+import com.narutohuo.xindazhou.community.model.Post
+import com.narutohuo.xindazhou.community.model.Topic
+import com.narutohuo.xindazhou.common.network.response.ApiResponse
+
+/**
+ * 社区数据仓库
+ */
+class CommunityRepository(
+    private val remoteDataSource: CommunityRemoteDataSource
+) {
+    
+    // ========== 帖子相关 ==========
+    
+    suspend fun getPostPage(
+        pageNo: Int,
+        pageSize: Int,
+        category: String? = null,
+        keyword: String? = null,
+        sort: String? = null,
+        city: String? = null,
+        topicId: Long? = null
+    ): ApiResponse<PageResult<Post>> {
+        return remoteDataSource.getPostPage(pageNo, pageSize, category, keyword, sort, city, topicId)
+    }
+    
+    suspend fun getPost(id: Long): ApiResponse<Post> {
+        return remoteDataSource.getPost(id)
+    }
+    
+    suspend fun createPost(request: CreatePostRequest): ApiResponse<Long> {
+        return remoteDataSource.createPost(request)
+    }
+    
+    suspend fun updatePost(request: UpdatePostRequest): ApiResponse<Boolean> {
+        return remoteDataSource.updatePost(request)
+    }
+    
+    suspend fun deletePost(id: Long): ApiResponse<Boolean> {
+        return remoteDataSource.deletePost(id)
+    }
+    
+    // ========== 评论相关 ==========
+    
+    suspend fun getCommentPage(
+        pageNo: Int,
+        pageSize: Int,
+        postId: Long
+    ): ApiResponse<PageResult<Comment>> {
+        return remoteDataSource.getCommentPage(pageNo, pageSize, postId)
+    }
+    
+    suspend fun getCommentTree(postId: Long): ApiResponse<List<Comment>> {
+        return remoteDataSource.getCommentTree(postId)
+    }
+    
+    suspend fun createComment(request: CreateCommentRequest): ApiResponse<Long> {
+        return remoteDataSource.createComment(request)
+    }
+    
+    suspend fun deleteComment(id: Long): ApiResponse<Boolean> {
+        return remoteDataSource.deleteComment(id)
+    }
+    
+    // ========== 互动相关 ==========
+    
+    suspend fun toggleLike(targetType: Int, targetId: Long): ApiResponse<Boolean> {
+        return remoteDataSource.toggleLike(LikeRequest(targetType, targetId))
+    }
+    
+    suspend fun toggleCollect(postId: Long): ApiResponse<Boolean> {
+        return remoteDataSource.toggleCollect(postId)
+    }
+    
+    suspend fun toggleFollow(followUserId: Long): ApiResponse<Boolean> {
+        return remoteDataSource.toggleFollow(followUserId)
+    }
+    
+    // ========== Banner相关 ==========
+    
+    suspend fun getBannerList(): ApiResponse<List<Banner>> {
+        return remoteDataSource.getBannerList()
+    }
+    
+    // ========== 话题相关 ==========
+    
+    suspend fun getTopicList(): ApiResponse<List<Topic>> {
+        return remoteDataSource.getTopicList()
+    }
+}
+

+ 315 - 55
app/src/main/java/com/narutohuo/xindazhou/community/ui/CommunityFragment.kt

@@ -1,16 +1,36 @@
 package com.narutohuo.xindazhou.community.ui
 
+import android.Manifest
+import android.content.Intent
+import android.content.pm.PackageManager
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
-import android.widget.Button
-import android.widget.LinearLayout
-import android.widget.TextView
 import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.viewModels
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.narutohuo.xindazhou.community.model.Post
+import com.narutohuo.xindazhou.community.model.Topic
+import com.narutohuo.xindazhou.community.ui.adapter.PostAdapter
+import com.narutohuo.xindazhou.community.ui.adapter.TopicAdapter
+import com.narutohuo.xindazhou.community.ui.content.PostDetailActivity
+import com.narutohuo.xindazhou.community.ui.dialog.CityPickerDialog
+import com.narutohuo.xindazhou.community.ui.viewmodel.CommunityState
+import com.narutohuo.xindazhou.community.ui.viewmodel.CommunityViewModel
+import com.narutohuo.xindazhou.community.ui.viewmodel.CommunityViewModelFactory
+import com.narutohuo.xindazhou.community.util.CityCacheHelper
+import com.narutohuo.xindazhou.community.util.LocationHelper
 import com.narutohuo.xindazhou.common.ui.BaseFragment
 import com.narutohuo.xindazhou.databinding.FragmentCommunityBinding
-import com.narutohuo.xindazhou.qrcode.factory.QRCodeManagerFactory
+import com.narutohuo.xindazhou.share.ShareKit
+import com.narutohuo.xindazhou.share.model.ShareContent
+import com.narutohuo.xindazhou.core.log.ILog
+import kotlinx.coroutines.launch
 
 /**
  * 社区 Fragment
@@ -19,75 +39,315 @@ import com.narutohuo.xindazhou.qrcode.factory.QRCodeManagerFactory
  */
 class CommunityFragment : BaseFragment<FragmentCommunityBinding>() {
     
-    private val qrCodeManager = QRCodeManagerFactory.getInstance()
+    private val viewModel: CommunityViewModel by viewModels {
+        CommunityViewModelFactory(requireActivity().application)
+    }
+    
+    private lateinit var postAdapter: PostAdapter
+    private lateinit var topicAdapter: TopicAdapter
+    
+    private val locationHelper by lazy { LocationHelper(requireContext()) }
+    private val cityCacheHelper by lazy { CityCacheHelper(requireContext()) }
+    
+    // 定位权限请求
+    private val locationPermissionLauncher = registerForActivityResult(
+        ActivityResultContracts.RequestMultiplePermissions()
+    ) { permissions ->
+        if (permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true ||
+            permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true) {
+            // 权限已授予,获取定位
+            getCurrentLocation()
+        } else {
+            // 权限被拒绝,使用缓存或默认城市
+            loadCachedCity()
+        }
+    }
+    
+    // 用于接收帖子详情页返回的评论数量更新
+    private val postDetailLauncher = registerForActivityResult(
+        ActivityResultContracts.StartActivityForResult()
+    ) { result ->
+        if (result.resultCode == android.app.Activity.RESULT_OK) {
+            val postId = result.data?.getLongExtra("postId", 0L) ?: 0L
+            val commentCount = result.data?.getIntExtra("commentCount", 0) ?: 0
+            if (postId > 0 && commentCount > 0) {
+                // 通过 ViewModel 更新帖子评论数量(符合 MVVM 架构)
+                viewModel.updatePostCommentCount(postId, commentCount)
+            }
+        }
+    }
     
     override fun getViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentCommunityBinding {
         return FragmentCommunityBinding.inflate(inflater, container, false)
     }
     
     override fun initView() {
-        // 创建布局
-        val layout = LinearLayout(requireContext()).apply {
-            orientation = LinearLayout.VERTICAL
-            gravity = android.view.Gravity.CENTER
-            setPadding(32, 32, 32, 32)
+        // 初始化帖子列表
+        postAdapter = PostAdapter(
+            onItemClick = { post ->
+                val intent = Intent(requireContext(), PostDetailActivity::class.java)
+                intent.putExtra("postId", post.id)
+                postDetailLauncher.launch(intent)
+            },
+            onLikeClick = { post ->
+                // 点赞功能已移除,保留参数以兼容
+            },
+            onCommentClick = { post ->
+                val intent = Intent(requireContext(), PostDetailActivity::class.java)
+                intent.putExtra("postId", post.id)
+                postDetailLauncher.launch(intent)
+            },
+            onCollectClick = { post ->
+                viewModel.toggleCollect(post)
+            },
+            onShareClick = { post ->
+                sharePost(post)
+            },
+            onFollowClick = { post ->
+                viewModel.toggleFollow(post)
+            }
+        )
+        
+        binding.rvPosts.layoutManager = LinearLayoutManager(requireContext())
+        binding.rvPosts.adapter = postAdapter
+        
+        // 初始化话题列表
+        topicAdapter = TopicAdapter { topic ->
+            viewModel.selectTopic(topic.id)
+            topicAdapter.setSelectedTopic(topic.id)
         }
-
-        // 标题
-        val textView = TextView(requireContext()).apply {
-            text = "社区页面"
-            textSize = 18f
-            gravity = android.view.Gravity.CENTER
-            layoutParams = LinearLayout.LayoutParams(
-                LinearLayout.LayoutParams.MATCH_PARENT,
-                LinearLayout.LayoutParams.WRAP_CONTENT
-            ).apply {
-                setMargins(0, 0, 0, 32)
+        binding.rvTopics.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
+        binding.rvTopics.adapter = topicAdapter
+        
+        // 下拉刷新
+        binding.swipeRefresh.setOnRefreshListener {
+            viewModel.refresh()
+        }
+        binding.swipeRefresh.setColorSchemeResources(com.narutohuo.xindazhou.R.color.accent_primary)
+        
+        // 设置点击事件
+        binding.ivMessage.setOnClickListener {
+            Toast.makeText(requireContext(), "消息功能待实现", Toast.LENGTH_SHORT).show()
+        }
+        
+        binding.ivPost.setOnClickListener {
+            val intent = Intent(requireContext(), com.narutohuo.xindazhou.community.ui.content.CreatePostActivity::class.java)
+            startActivity(intent)
+        }
+        
+        // 标签切换:推荐、地区、关注
+        binding.tvTabRecommended.setOnClickListener { 
+            switchTab(0, "recommended", null)
+        }
+        binding.tvTabCity.setOnClickListener {
+            // 点击地区标签,打开城市选择器
+            showCityPicker()
+        }
+        binding.tvTabFollow.setOnClickListener {
+            // 关注:需要传递当前用户ID,暂时使用null
+            switchTab(2, null, null)
+        }
+        
+        // 搜索
+        binding.etSearch.setOnEditorActionListener { _, _, _ ->
+            val keyword = binding.etSearch.text.toString().trim()
+            if (keyword.isNotEmpty()) {
+                viewModel.search(keyword)
             }
+            true
         }
-        layout.addView(textView)
-
-        // 扫码按钮
-        val scanButton = Button(requireContext()).apply {
-            text = "扫码"
-            layoutParams = LinearLayout.LayoutParams(
-                LinearLayout.LayoutParams.WRAP_CONTENT,
-                LinearLayout.LayoutParams.WRAP_CONTENT
-            )
-            setOnClickListener {
-                startScanQRCode()
+        
+        // 观察状态
+        observeState()
+        
+        // 初始化城市(从缓存或定位获取)
+        initCity()
+        
+        // 初始加载
+        viewModel.loadPosts()
+    }
+    
+    private fun switchTab(index: Int, category: String?, city: String?) {
+        val tabs = listOf(binding.tvTabRecommended, binding.tvTabCity, binding.tvTabFollow)
+        tabs.forEachIndexed { i, tab ->
+            if (i == index) {
+                tab.setTextColor(requireContext().getColor(com.narutohuo.xindazhou.R.color.accent_primary))
+                tab.setTypeface(null, android.graphics.Typeface.BOLD)
+                tab.background = requireContext().getDrawable(com.narutohuo.xindazhou.R.drawable.bg_tab_selected)
+            } else {
+                tab.setTextColor(requireContext().getColor(com.narutohuo.xindazhou.R.color.text_tertiary))
+                tab.setTypeface(null, android.graphics.Typeface.NORMAL)
+                tab.background = requireContext().getDrawable(com.narutohuo.xindazhou.R.drawable.bg_tab_unselected)
             }
         }
-        layout.addView(scanButton)
         
-        // 将内容添加到布局中
-        binding.contentContainer.removeAllViews()
-        binding.contentContainer.addView(layout)
+        // 切换分类或城市
+        if (category != null) {
+            viewModel.switchCategory(category)
+        } else if (city != null) {
+            viewModel.setCity(city)
+        } else {
+            // 关注:暂时使用null,后续需要实现关注用户筛选
+            viewModel.switchCategory(null)
+        }
     }
     
     /**
-     * 开始扫码
+     * 初始化城市
      */
-    private fun startScanQRCode() {
-        qrCodeManager.scanQRCode(requireActivity()) { response ->
-            if (response.success) {
-                val qrCodeContent = response.data
-                Toast.makeText(
-                    requireContext(),
-                    "扫码成功:$qrCodeContent",
-                    Toast.LENGTH_SHORT
-                ).show()
-                // TODO: 处理扫码结果
-            } else {
-                val error = response.errorMessage
-                if (error != null) {
-                    Toast.makeText(
-                        requireContext(),
-                        "扫码失败:$error",
-                        Toast.LENGTH_SHORT
-                    ).show()
+    private fun initCity() {
+        // 先尝试从缓存获取
+        val cachedCity = cityCacheHelper.getCurrentCity() ?: cityCacheHelper.getCachedLocationCity()
+        if (cachedCity != null) {
+            viewModel.setCity(cachedCity)
+            updateCityTabText(cachedCity)
+            return
+        }
+        
+        // 如果没有缓存,尝试定位
+        if (locationHelper.hasLocationPermission()) {
+            getCurrentLocation()
+        } else {
+            // 没有权限,显示默认
+            updateCityTabText("全国")
+        }
+    }
+    
+    /**
+     * 获取当前位置
+     */
+    private fun getCurrentLocation() {
+        lifecycleScope.launch {
+            try {
+                val city = locationHelper.getCurrentCity()
+                if (city != null) {
+                    // 保存到缓存
+                    cityCacheHelper.saveLocationCity(city)
+                    cityCacheHelper.saveCurrentCity(city)
+                    viewModel.setCity(city)
+                    updateCityTabText(city)
+                } else {
+                    // 定位失败,使用默认
+                    updateCityTabText("全国")
+                }
+            } catch (e: Exception) {
+                ILog.e("CommunityFragment", "获取定位失败", e)
+                updateCityTabText("全国")
+            }
+        }
+    }
+    
+    /**
+     * 加载缓存的城市
+     */
+    private fun loadCachedCity() {
+        val cachedCity = cityCacheHelper.getCurrentCity() ?: cityCacheHelper.getCachedLocationCity()
+        if (cachedCity != null) {
+            viewModel.setCity(cachedCity)
+            updateCityTabText(cachedCity)
+        } else {
+            updateCityTabText("全国")
+        }
+    }
+    
+    /**
+     * 显示城市选择器
+     */
+    private fun showCityPicker() {
+        val dialog = CityPickerDialog.newInstance { city ->
+            cityCacheHelper.saveCurrentCity(city)
+            viewModel.setCity(city)
+            updateCityTabText(city)
+            switchTab(1, null, city)
+        }
+        dialog.show(parentFragmentManager, "CityPickerDialog")
+    }
+    
+    /**
+     * 更新地区标签文本
+     */
+    private fun updateCityTabText(city: String) {
+        binding.tvTabCity.text = city
+    }
+    
+    private fun observeState() {
+        // 观察帖子列表状态
+        lifecycleScope.launch {
+            viewModel.communityState.collect { state ->
+                when (state) {
+                    is CommunityState.Idle -> {
+                        // 初始状态
+                    }
+                    is CommunityState.Loading -> {
+                        showLoading()
+                        binding.swipeRefresh.isRefreshing = true
+                    }
+                    is CommunityState.Success -> {
+                        hideLoading()
+                        binding.swipeRefresh.isRefreshing = false
+                        postAdapter.submitList(state.posts)
+                    }
+                    is CommunityState.Error -> {
+                        hideLoading()
+                        binding.swipeRefresh.isRefreshing = false
+                        showError(state.message)
+                    }
+                }
+            }
+        }
+        
+        // 观察话题列表
+        lifecycleScope.launch {
+            viewModel.topicsState.collect { topics ->
+                topicAdapter.submitList(topics)
+            }
+        }
+        
+        // 观察当前城市
+        lifecycleScope.launch {
+            viewModel.currentCity.collect { city ->
+                if (city != null) {
+                    updateCityTabText(city)
+                }
+            }
+        }
+    }
+    
+    /**
+     * 分享帖子
+     */
+    private fun sharePost(post: Post) {
+        try {
+            val shareService = ShareKit.getInstance()
+            
+            // 构建分享链接(TODO: 使用实际的帖子详情页链接)
+            val postUrl = "https://www.xindazhou.com/community/post/${post.id}"
+            
+            // 创建分享内容
+            val shareContent = ShareContent.builder()
+                .setTitle(post.title)
+                .setDescription(post.content ?: "")
+                .setUrl(postUrl)
+                .setImageUrl(post.mediaList?.firstOrNull()?.mediaUrl) // 使用第一张图片作为分享图片
+                .build()
+            
+            // 调用分享(不指定平台,会显示分享弹窗)
+            // ShareProxyActivity 会自动处理回调并关闭,回调已经在主线程
+            shareService.share(requireActivity(), shareContent) { response ->
+                if (response.success) {
+                    val platformName = response.data?.name ?: "未知平台"
+                    Toast.makeText(requireContext(), "分享成功:$platformName", Toast.LENGTH_SHORT).show()
+                } else {
+                    // 用户取消分享时不显示 Toast
+                    if (response.errorMessage != "用户取消分享") {
+                        val errorMsg = response.errorMessage ?: "分享失败"
+                        Toast.makeText(requireContext(), errorMsg, Toast.LENGTH_SHORT).show()
+                    }
                 }
             }
+        } catch (e: Exception) {
+            ILog.e("CommunityFragment", "分享功能异常", e)
+            Toast.makeText(requireContext(), "分享功能异常:${e.message}", Toast.LENGTH_SHORT).show()
         }
     }
 }

+ 254 - 0
app/src/main/java/com/narutohuo/xindazhou/community/ui/adapter/CommentAdapter.kt

@@ -0,0 +1,254 @@
+package com.narutohuo.xindazhou.community.ui.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.narutohuo.xindazhou.community.model.Comment
+import com.narutohuo.xindazhou.databinding.ItemCommentBinding
+import java.text.SimpleDateFormat
+import java.util.*
+
+/**
+ * 评论列表 Adapter
+ * 支持嵌套回复显示
+ */
+class CommentAdapter(
+    private val onItemClick: (Comment) -> Unit,
+    private val onLikeClick: (Comment) -> Unit,
+    private val onReplyClick: (Comment) -> Unit
+) : ListAdapter<Comment, CommentAdapter.CommentViewHolder>(CommentDiffCallback()) {
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentViewHolder {
+        val binding = ItemCommentBinding.inflate(
+            LayoutInflater.from(parent.context),
+            parent,
+            false
+        )
+        return CommentViewHolder(binding)
+    }
+
+    override fun onBindViewHolder(holder: CommentViewHolder, position: Int) {
+        holder.bind(getItem(position))
+    }
+
+    inner class CommentViewHolder(
+        private val binding: ItemCommentBinding
+    ) : RecyclerView.ViewHolder(binding.root) {
+
+        private val replyAdapter = ReplyAdapter()
+
+        init {
+            binding.rvReplies.layoutManager = LinearLayoutManager(binding.root.context)
+            binding.rvReplies.adapter = replyAdapter
+        }
+
+        fun bind(comment: Comment, isReply: Boolean = false) {
+            // 如果是回复,添加左侧缩进
+            if (isReply) {
+                val marginStart = binding.root.context.resources.getDimensionPixelSize(
+                    com.narutohuo.xindazhou.R.dimen.comment_reply_indent
+                )
+                (binding.root.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
+                    it.marginStart = marginStart
+                    binding.root.layoutParams = it
+                }
+            } else {
+                (binding.root.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
+                    it.marginStart = 0
+                    binding.root.layoutParams = it
+                }
+            }
+            
+            // 用户信息
+            binding.tvNickname.text = comment.userNickname ?: "匿名用户"
+            binding.tvTime.text = formatTime(comment.createTime)
+            
+            // 评论内容
+            if (comment.parentUserNickname != null && comment.parentId > 0) {
+                // 回复评论,显示 "@用户名"
+                binding.tvContent.text = "@${comment.parentUserNickname} ${comment.content}"
+            } else {
+                binding.tvContent.text = comment.content
+            }
+            
+            // 点赞数
+            binding.tvLikeCount.text = comment.likeCount.toString()
+            
+            // 点赞状态
+            val likeIcon = if (comment.isLiked == true) {
+                android.R.drawable.star_big_on // TODO: 使用自定义图标
+            } else {
+                android.R.drawable.star_big_off // TODO: 使用自定义图标
+            }
+            binding.ivLike.setImageResource(likeIcon)
+            binding.ivLike.setColorFilter(
+                if (comment.isLiked == true) {
+                    binding.root.context.getColor(com.narutohuo.xindazhou.R.color.accent_primary)
+                } else {
+                    binding.root.context.getColor(com.narutohuo.xindazhou.R.color.text_tertiary)
+                }
+            )
+            
+            // 点击事件
+            binding.root.setOnClickListener { onItemClick(comment) }
+            binding.llLike.setOnClickListener { onLikeClick(comment) }
+            binding.tvReply.setOnClickListener { onReplyClick(comment) }
+            
+            // 显示回复列表(嵌套回复)
+            val replies = comment.replies ?: emptyList()
+            if (replies.isEmpty()) {
+                binding.rvReplies.visibility = View.GONE
+            } else {
+                binding.rvReplies.visibility = View.VISIBLE
+                replyAdapter.submitList(replies)
+            }
+        }
+        
+        // 内部 Adapter 用于显示回复列表
+        inner class ReplyAdapter : RecyclerView.Adapter<ReplyViewHolder>() {
+            private val replies = mutableListOf<Comment>()
+            
+            fun submitList(newReplies: List<Comment>) {
+                replies.clear()
+                replies.addAll(newReplies)
+                notifyDataSetChanged()
+            }
+            
+            override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReplyViewHolder {
+                val binding = ItemCommentBinding.inflate(
+                    LayoutInflater.from(parent.context),
+                    parent,
+                    false
+                )
+                return ReplyViewHolder(binding)
+            }
+            
+            override fun onBindViewHolder(holder: ReplyViewHolder, position: Int) {
+                holder.bind(replies[position])
+            }
+            
+            override fun getItemCount() = replies.size
+        }
+        
+        // 回复 ViewHolder(简化版,不需要嵌套的回复列表)
+        inner class ReplyViewHolder(
+            private val binding: ItemCommentBinding
+        ) : RecyclerView.ViewHolder(binding.root) {
+            
+            init {
+                // 回复不需要显示嵌套的回复列表,隐藏 rvReplies
+                binding.rvReplies.visibility = View.GONE
+            }
+            
+            fun bind(comment: Comment) {
+                // 添加左侧缩进,显示在父评论下面(减少缩进距离,更美观)
+                val marginStart = binding.root.context.resources.getDimensionPixelSize(
+                    com.narutohuo.xindazhou.R.dimen.comment_reply_indent
+                )
+                (binding.root.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
+                    it.marginStart = marginStart
+                    binding.root.layoutParams = it
+                }
+                
+                // 优化回复的视觉样式:减少内边距,使回复更紧凑
+                val paddingSmall = binding.root.context.resources.getDimensionPixelSize(
+                    com.narutohuo.xindazhou.R.dimen.spacing_small
+                )
+                binding.root.setPadding(
+                    binding.root.paddingLeft,
+                    paddingSmall, // 8dp 上边距
+                    binding.root.paddingRight,
+                    paddingSmall  // 8dp 下边距
+                )
+                
+                // 优化回复的背景色,使其与主评论区分(使用浅灰色背景)
+                binding.root.setBackgroundColor(
+                    binding.root.context.getColor(com.narutohuo.xindazhou.R.color.bg_secondary)
+                )
+                
+                // 用户信息
+                binding.tvNickname.text = comment.userNickname ?: "匿名用户"
+                binding.tvTime.text = formatTime(comment.createTime)
+                
+                // 评论内容(回复显示 "@用户名 内容")
+                if (comment.parentUserNickname != null && comment.parentId > 0) {
+                    binding.tvContent.text = "@${comment.parentUserNickname} ${comment.content}"
+                } else {
+                    binding.tvContent.text = comment.content
+                }
+                
+                // 点赞数
+                binding.tvLikeCount.text = comment.likeCount.toString()
+                
+                // 点赞状态
+                val likeIcon = if (comment.isLiked == true) {
+                    android.R.drawable.star_big_on
+                } else {
+                    android.R.drawable.star_big_off
+                }
+                binding.ivLike.setImageResource(likeIcon)
+                binding.ivLike.setColorFilter(
+                    if (comment.isLiked == true) {
+                        binding.root.context.getColor(com.narutohuo.xindazhou.R.color.accent_primary)
+                    } else {
+                        binding.root.context.getColor(com.narutohuo.xindazhou.R.color.text_tertiary)
+                    }
+                )
+                
+                // 点击事件
+                binding.root.setOnClickListener { onItemClick(comment) }
+                binding.llLike.setOnClickListener { onLikeClick(comment) }
+                binding.tvReply.setOnClickListener { onReplyClick(comment) }
+            }
+            
+            private fun formatTime(timestamp: Long?): String {
+                if (timestamp == null) return ""
+                val now = System.currentTimeMillis()
+                val diff = now - timestamp
+                
+                return when {
+                    diff < 60_000 -> "刚刚"
+                    diff < 3600_000 -> "${diff / 60_000}分钟前"
+                    diff < 86400_000 -> "${diff / 3600_000}小时前"
+                    diff < 604800_000 -> "${diff / 86400_000}天前"
+                    else -> {
+                        val sdf = SimpleDateFormat("MM-dd", Locale.getDefault())
+                        sdf.format(Date(timestamp))
+                    }
+                }
+            }
+        }
+        
+        private fun formatTime(timestamp: Long?): String {
+            if (timestamp == null) return ""
+            val now = System.currentTimeMillis()
+            val diff = now - timestamp
+            
+            return when {
+                diff < 60_000 -> "刚刚"
+                diff < 3600_000 -> "${diff / 60_000}分钟前"
+                diff < 86400_000 -> "${diff / 3600_000}小时前"
+                diff < 604800_000 -> "${diff / 86400_000}天前"
+                else -> {
+                    val sdf = SimpleDateFormat("MM-dd", Locale.getDefault())
+                    sdf.format(Date(timestamp))
+                }
+            }
+        }
+    }
+
+    private class CommentDiffCallback : DiffUtil.ItemCallback<Comment>() {
+        override fun areItemsTheSame(oldItem: Comment, newItem: Comment): Boolean {
+            return oldItem.id == newItem.id
+        }
+
+        override fun areContentsTheSame(oldItem: Comment, newItem: Comment): Boolean {
+            return oldItem == newItem
+        }
+    }
+}
+

+ 65 - 0
app/src/main/java/com/narutohuo/xindazhou/community/ui/adapter/MediaAdapter.kt

@@ -0,0 +1,65 @@
+package com.narutohuo.xindazhou.community.ui.adapter
+
+import android.net.Uri
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.ImageView
+import androidx.recyclerview.widget.RecyclerView
+import com.narutohuo.xindazhou.common.image.ImageLoadHelper
+import com.narutohuo.xindazhou.community.ui.viewmodel.MediaItem
+import com.narutohuo.xindazhou.databinding.ItemMediaBinding
+
+/**
+ * 媒体适配器(用于显示已选择的图片/视频)
+ */
+class MediaAdapter(
+    private val mediaItems: List<MediaItem>,
+    private val onItemClick: (MediaItem) -> Unit,
+    private val onRemoveClick: (Int) -> Unit
+) : RecyclerView.Adapter<MediaAdapter.MediaViewHolder>() {
+    
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
+        val binding = ItemMediaBinding.inflate(
+            LayoutInflater.from(parent.context),
+            parent,
+            false
+        )
+        return MediaViewHolder(binding)
+    }
+    
+    override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
+        holder.bind(mediaItems[position], position)
+    }
+    
+    override fun getItemCount(): Int = mediaItems.size
+    
+    inner class MediaViewHolder(
+        private val binding: ItemMediaBinding
+    ) : RecyclerView.ViewHolder(binding.root) {
+        
+        fun bind(item: MediaItem, position: Int) {
+            // 加载图片或视频缩略图
+            ImageLoadHelper.load(item.uri, binding.ivMedia)
+            
+            // 显示视频图标
+            binding.ivVideoIcon.visibility = if (item.type == MediaItem.MediaType.VIDEO) {
+                View.VISIBLE
+            } else {
+                View.GONE
+            }
+            
+            // 点击预览
+            binding.root.setOnClickListener {
+                onItemClick(item)
+            }
+            
+            // 删除按钮
+            binding.ibRemove.setOnClickListener {
+                onRemoveClick(position)
+            }
+        }
+    }
+}
+

+ 119 - 0
app/src/main/java/com/narutohuo/xindazhou/community/ui/adapter/PostAdapter.kt

@@ -0,0 +1,119 @@
+package com.narutohuo.xindazhou.community.ui.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.narutohuo.xindazhou.community.model.Post
+import com.narutohuo.xindazhou.databinding.ItemCommunityPostBinding
+import java.text.SimpleDateFormat
+import java.util.*
+
+/**
+ * 帖子列表 Adapter
+ */
+class PostAdapter(
+    private val onItemClick: (Post) -> Unit,
+    private val onLikeClick: (Post) -> Unit,
+    private val onCommentClick: (Post) -> Unit,
+    private val onCollectClick: (Post) -> Unit,
+    private val onShareClick: (Post) -> Unit,
+    private val onFollowClick: (Post) -> Unit
+) : ListAdapter<Post, PostAdapter.PostViewHolder>(PostDiffCallback()) {
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
+        val binding = ItemCommunityPostBinding.inflate(
+            LayoutInflater.from(parent.context),
+            parent,
+            false
+        )
+        return PostViewHolder(binding)
+    }
+
+    override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
+        holder.bind(getItem(position))
+    }
+
+    inner class PostViewHolder(
+        private val binding: ItemCommunityPostBinding
+    ) : RecyclerView.ViewHolder(binding.root) {
+
+        fun bind(post: Post) {
+            // 用户信息
+            binding.tvNickname.text = post.userNickname ?: "匿名用户"
+            binding.tvTime.text = formatTime(post.createTime)
+            
+            // 头像(TODO: 使用图片加载库加载)
+            // Glide.with(binding.root).load(post.userAvatar).into(binding.ivAvatar)
+            
+            // 关注按钮
+            binding.tvFollow.text = if (post.isFollowed == true) "已关注" else "关注"
+            binding.tvFollow.setOnClickListener { onFollowClick(post) }
+            
+            // 帖子内容
+            binding.tvTitle.text = post.title
+            binding.tvContent.text = post.content ?: ""
+            binding.tvContent.visibility = if (post.content.isNullOrBlank()) View.GONE else View.VISIBLE
+            
+            // 媒体列表(TODO: 实现图片/视频展示)
+            binding.rvMedia.visibility = View.GONE
+            
+            // 互动数据
+            binding.tvCommentCount.text = post.commentCount.toString()
+            binding.tvCollectCount.text = post.collectCount.toString()
+            
+            // 收藏状态
+            val collectIcon = if (post.isCollected == true) {
+                android.R.drawable.star_big_on // TODO: 使用自定义图标
+            } else {
+                android.R.drawable.star_big_off // TODO: 使用自定义图标
+            }
+            binding.ivCollect.setImageResource(collectIcon)
+            binding.ivCollect.setColorFilter(
+                if (post.isCollected == true) {
+                    binding.root.context.getColor(com.narutohuo.xindazhou.R.color.accent_primary)
+                } else {
+                    binding.root.context.getColor(com.narutohuo.xindazhou.R.color.text_tertiary)
+                }
+            )
+            
+            // 点击事件
+            binding.root.setOnClickListener { onItemClick(post) }
+            binding.llComment.setOnClickListener { onCommentClick(post) }
+            binding.llCollect.setOnClickListener { onCollectClick(post) }
+            binding.llShare.setOnClickListener { onShareClick(post) }
+        }
+        
+        private fun formatTime(timestamp: Long?): String {
+            if (timestamp == null) return ""
+            val now = System.currentTimeMillis()
+            val diff = now - timestamp
+            
+            return when {
+                diff < 60_000 -> "刚刚"
+                diff < 3600_000 -> "${diff / 60_000}分钟前"
+                diff < 86400_000 -> "${diff / 3600_000}小时前"
+                diff < 604800_000 -> "${diff / 86400_000}天前"
+                else -> {
+                    val sdf = SimpleDateFormat("MM-dd", Locale.getDefault())
+                    sdf.format(Date(timestamp))
+                }
+            }
+        }
+    }
+
+    private class PostDiffCallback : DiffUtil.ItemCallback<Post>() {
+        override fun areItemsTheSame(oldItem: Post, newItem: Post): Boolean {
+            return oldItem.id == newItem.id
+        }
+
+        override fun areContentsTheSame(oldItem: Post, newItem: Post): Boolean {
+            return oldItem == newItem
+        }
+    }
+}
+

+ 78 - 0
app/src/main/java/com/narutohuo/xindazhou/community/ui/adapter/TopicAdapter.kt

@@ -0,0 +1,78 @@
+package com.narutohuo.xindazhou.community.ui.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.narutohuo.xindazhou.community.model.Topic
+import com.narutohuo.xindazhou.databinding.ItemTopicBinding
+
+/**
+ * 话题列表 Adapter
+ */
+class TopicAdapter(
+    private val onItemClick: (Topic) -> Unit
+) : ListAdapter<Topic, TopicAdapter.TopicViewHolder>(TopicDiffCallback()) {
+
+    private var selectedTopicId: Long? = null
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TopicViewHolder {
+        val binding = ItemTopicBinding.inflate(
+            LayoutInflater.from(parent.context),
+            parent,
+            false
+        )
+        return TopicViewHolder(binding)
+    }
+
+    override fun onBindViewHolder(holder: TopicViewHolder, position: Int) {
+        holder.bind(getItem(position), selectedTopicId == getItem(position).id)
+    }
+
+    fun setSelectedTopic(topicId: Long?) {
+        val oldSelectedId = selectedTopicId
+        selectedTopicId = topicId
+        // 更新选中状态
+        currentList.forEachIndexed { index, topic ->
+            if (topic.id == oldSelectedId || topic.id == topicId) {
+                notifyItemChanged(index)
+            }
+        }
+    }
+
+    inner class TopicViewHolder(
+        private val binding: ItemTopicBinding
+    ) : RecyclerView.ViewHolder(binding.root) {
+
+        fun bind(topic: Topic, isSelected: Boolean) {
+            binding.tvTopicName.text = topic.name
+            
+            // 更新选中状态
+            if (isSelected) {
+                binding.root.setBackgroundResource(com.narutohuo.xindazhou.R.drawable.bg_tab_selected)
+                binding.tvTopicName.setTextColor(binding.root.context.getColor(com.narutohuo.xindazhou.R.color.accent_primary))
+            } else {
+                binding.root.setBackgroundResource(com.narutohuo.xindazhou.R.drawable.bg_tab_unselected)
+                binding.tvTopicName.setTextColor(binding.root.context.getColor(com.narutohuo.xindazhou.R.color.text_tertiary))
+            }
+            
+            binding.root.setOnClickListener {
+                onItemClick(topic)
+            }
+        }
+    }
+
+    private class TopicDiffCallback : DiffUtil.ItemCallback<Topic>() {
+        override fun areItemsTheSame(oldItem: Topic, newItem: Topic): Boolean {
+            return oldItem.id == newItem.id
+        }
+
+        override fun areContentsTheSame(oldItem: Topic, newItem: Topic): Boolean {
+            return oldItem == newItem
+        }
+    }
+}
+

+ 328 - 0
app/src/main/java/com/narutohuo/xindazhou/community/ui/content/CreatePostActivity.kt

@@ -0,0 +1,328 @@
+package com.narutohuo.xindazhou.community.ui.content
+
+import android.Manifest
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import android.widget.Toast
+import androidx.activity.viewModels
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.GridLayoutManager
+import com.narutohuo.xindazhou.common.image.ImageLoadHelper
+import com.narutohuo.xindazhou.common.media.MediaPickerHelper
+import com.narutohuo.xindazhou.community.ui.adapter.MediaAdapter
+import com.narutohuo.xindazhou.community.ui.viewmodel.CreatePostState
+import com.narutohuo.xindazhou.community.ui.viewmodel.CreatePostViewModel
+import com.narutohuo.xindazhou.community.ui.viewmodel.CommunityViewModelFactory
+import com.narutohuo.xindazhou.community.ui.viewmodel.MediaItem
+import com.narutohuo.xindazhou.common.ui.BaseActivity
+import com.narutohuo.xindazhou.databinding.ActivityCreatePostBinding
+import kotlinx.coroutines.launch
+
+/**
+ * 发帖 Activity
+ */
+class CreatePostActivity : BaseActivity<ActivityCreatePostBinding>() {
+    
+    private val viewModel: CreatePostViewModel by viewModels {
+        CommunityViewModelFactory(application)
+    }
+    
+    private lateinit var mediaPickerHelper: MediaPickerHelper
+    private lateinit var mediaAdapter: MediaAdapter
+    
+    override fun getViewBinding(): ActivityCreatePostBinding {
+        return ActivityCreatePostBinding.inflate(layoutInflater)
+    }
+    
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        
+        // 初始化媒体选择器
+        mediaPickerHelper = MediaPickerHelper(this)
+        
+        initView()
+        observeState()
+    }
+    
+    override fun initView() {
+        // 取消按钮
+        binding.tvCancel.setOnClickListener {
+            handleCancel()
+        }
+        
+        // 发布按钮
+        binding.tvPublish.setOnClickListener {
+            publishPost()
+        }
+        
+        // 封面图片点击
+        binding.llCoverPlaceholder.setOnClickListener {
+            selectCoverImage()
+        }
+        
+        // 封面图片点击(已有封面时)
+        binding.ivCover.setOnClickListener {
+            selectCoverImage()
+        }
+        
+        // 删除封面
+        binding.ibRemoveCover.setOnClickListener {
+            removeCoverImage()
+        }
+        
+        // 添加图片
+        binding.ivAddImage.setOnClickListener {
+            selectImages()
+        }
+        
+        // 添加视频
+        binding.ivAddVideo.setOnClickListener {
+            selectVideo()
+        }
+        
+        // 初始化媒体列表
+        mediaAdapter = MediaAdapter(
+            mediaItems = emptyList(),
+            onItemClick = { item ->
+                // TODO: 预览媒体
+                Toast.makeText(this, "预览功能待实现", Toast.LENGTH_SHORT).show()
+            },
+            onRemoveClick = { index ->
+                viewModel.removeMedia(index)
+                updateMediaList()
+            }
+        )
+        
+        binding.rvMedia.layoutManager = GridLayoutManager(this, 2)
+        binding.rvMedia.adapter = mediaAdapter
+        
+        // 初始状态:隐藏媒体列表
+        binding.rvMedia.visibility = View.GONE
+    }
+    
+    private fun observeState() {
+        lifecycleScope.launch {
+            viewModel.createPostState.collect { state ->
+                when (state) {
+                    is CreatePostState.Idle -> {
+                        // 初始状态
+                    }
+                    is CreatePostState.Loading -> {
+                        showLoading()
+                    }
+                    is CreatePostState.Success -> {
+                        hideLoading()
+                        Toast.makeText(this@CreatePostActivity, "发布成功", Toast.LENGTH_SHORT).show()
+                        // 返回结果
+                        setResult(RESULT_OK)
+                        finish()
+                    }
+                    is CreatePostState.Error -> {
+                        hideLoading()
+                        showError(state.message)
+                    }
+                }
+            }
+        }
+    }
+    
+    /**
+     * 选择封面图片
+     */
+    private fun selectCoverImage() {
+        // Android 11+ 使用 Photo Picker API 不需要权限,但为了兼容低版本仍然请求
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
+            // Android 11+ 直接使用,不需要权限
+            mediaPickerHelper.selectImages(maxCount = 1) { uris ->
+                if (uris.isNotEmpty()) {
+                    val uri = uris.first()
+                    viewModel.setCoverImage(uri)
+                    updateCoverImage(uri)
+                }
+            }
+        } else {
+            // Android 10 及以下需要权限
+            requestPermissions(
+                *MediaPickerHelper.getRequiredPermissions(),
+                onGranted = {
+                    mediaPickerHelper.selectImages(maxCount = 1) { uris ->
+                        if (uris.isNotEmpty()) {
+                            val uri = uris.first()
+                            viewModel.setCoverImage(uri)
+                            updateCoverImage(uri)
+                        }
+                    }
+                },
+                onDenied = {
+                    showError("需要相机和存储权限才能选择图片")
+                }
+            )
+        }
+    }
+    
+    /**
+     * 更新封面图片显示
+     */
+    private fun updateCoverImage(uri: Uri) {
+        ImageLoadHelper.load(uri, binding.ivCover)
+        binding.ivCover.visibility = View.VISIBLE
+        binding.llCoverPlaceholder.visibility = View.GONE
+        binding.ibRemoveCover.visibility = View.VISIBLE
+    }
+    
+    /**
+     * 删除封面图片
+     */
+    private fun removeCoverImage() {
+        viewModel.clearCoverImage()
+        binding.ivCover.visibility = View.GONE
+        binding.llCoverPlaceholder.visibility = View.VISIBLE
+        binding.ibRemoveCover.visibility = View.GONE
+    }
+    
+    /**
+     * 选择图片(打开系统相册,支持多选,最多9张)
+     * 系统相册界面有相机入口,用户可以拍照
+     */
+    private fun selectImages() {
+        val currentImageCount = viewModel.getMediaItems().count { it.type == MediaItem.MediaType.IMAGE }
+        val maxCount = 9 - currentImageCount
+        
+        if (maxCount <= 0) {
+            showError("图片最多只能添加9张")
+            return
+        }
+        
+        // Android 11+ 使用 Photo Picker API 不需要权限,但为了兼容低版本仍然请求
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
+            // Android 11+ 直接使用,不需要权限
+            mediaPickerHelper.selectImages(maxCount = maxCount) { uris ->
+                viewModel.addImages(uris)
+                updateMediaList()
+            }
+        } else {
+            // Android 10 及以下需要权限
+            requestPermissions(
+                *MediaPickerHelper.getRequiredPermissions(),
+                onGranted = {
+                    mediaPickerHelper.selectImages(maxCount = maxCount) { uris ->
+                        viewModel.addImages(uris)
+                        updateMediaList()
+                    }
+                },
+                onDenied = {
+                    showError("需要相机和存储权限才能选择图片")
+                }
+            )
+        }
+    }
+    
+    /**
+     * 选择视频(打开系统相册,单选)
+     * 系统相册界面有相机入口,用户可以录像
+     */
+    private fun selectVideo() {
+        // 检查是否已有视频
+        if (viewModel.getMediaItems().any { it.type == MediaItem.MediaType.VIDEO }) {
+            showError("视频最多只能添加1个")
+            return
+        }
+        
+        // Android 11+ 使用 Photo Picker API 不需要权限,但为了兼容低版本仍然请求
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
+            // Android 11+ 直接使用,不需要权限
+            mediaPickerHelper.selectVideo { uri ->
+                viewModel.addVideo(uri)
+                updateMediaList()
+            }
+        } else {
+            // Android 10 及以下需要权限
+            requestPermissions(
+                *MediaPickerHelper.getRequiredPermissions(),
+                onGranted = {
+                    mediaPickerHelper.selectVideo { uri ->
+                        viewModel.addVideo(uri)
+                        updateMediaList()
+                    }
+                },
+                onDenied = {
+                    showError("需要相机和存储权限才能选择视频")
+                }
+            )
+        }
+    }
+    
+    /**
+     * 更新媒体列表显示
+     */
+    private fun updateMediaList() {
+        val mediaItems = viewModel.getMediaItems()
+        if (mediaItems.isEmpty()) {
+            binding.rvMedia.visibility = View.GONE
+        } else {
+            binding.rvMedia.visibility = View.VISIBLE
+            mediaAdapter = MediaAdapter(
+                mediaItems = mediaItems,
+                onItemClick = { item ->
+                    // TODO: 预览媒体
+                    Toast.makeText(this, "预览功能待实现", Toast.LENGTH_SHORT).show()
+                },
+                onRemoveClick = { index ->
+                    viewModel.removeMedia(index)
+                    updateMediaList()
+                }
+            )
+            binding.rvMedia.adapter = mediaAdapter
+        }
+    }
+    
+    /**
+     * 发布帖子
+     */
+    private fun publishPost() {
+        val title = binding.etTitle.text.toString().trim()
+        val content = binding.etContent.text.toString().trim()
+        
+        if (title.isEmpty()) {
+            showError("请输入标题")
+            return
+        }
+        
+        viewModel.publishPost(
+            title = title,
+            content = if (content.isEmpty()) null else content,
+            tags = null // TODO: 支持标签输入
+        )
+    }
+    
+    /**
+     * 处理取消
+     */
+    private fun handleCancel() {
+        val title = binding.etTitle.text.toString().trim()
+        val content = binding.etContent.text.toString().trim()
+        val hasMedia = viewModel.getMediaItems().isNotEmpty() || viewModel.coverImageUri != null
+        
+        if (title.isNotEmpty() || content.isNotEmpty() || hasMedia) {
+            // 有未保存内容,显示确认对话框
+            showConfirm(
+                title = "确认取消",
+                message = "您有未保存的内容,确定要取消吗?",
+                onConfirm = {
+                    finish()
+                }
+            )
+        } else {
+            // 没有内容,直接关闭
+            finish()
+        }
+    }
+    
+    override fun onBackKeyPressed(): Boolean {
+        handleCancel()
+        return true
+    }
+}
+

+ 314 - 0
app/src/main/java/com/narutohuo/xindazhou/community/ui/content/PostDetailActivity.kt

@@ -0,0 +1,314 @@
+package com.narutohuo.xindazhou.community.ui.content
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import android.widget.Toast
+import androidx.activity.viewModels
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.narutohuo.xindazhou.community.model.Comment
+import com.narutohuo.xindazhou.community.model.Post
+import com.narutohuo.xindazhou.community.ui.adapter.CommentAdapter
+import com.narutohuo.xindazhou.community.ui.viewmodel.CommentState
+import com.narutohuo.xindazhou.community.ui.viewmodel.PostCommentState
+import com.narutohuo.xindazhou.community.ui.viewmodel.PostDetailState
+import com.narutohuo.xindazhou.community.ui.viewmodel.PostDetailViewModel
+import com.narutohuo.xindazhou.community.ui.viewmodel.CommunityViewModelFactory
+import com.narutohuo.xindazhou.common.ui.BaseActivity
+import com.narutohuo.xindazhou.databinding.ActivityPostDetailBinding
+import com.narutohuo.xindazhou.share.ShareKit
+import com.narutohuo.xindazhou.share.model.ShareContent
+import com.narutohuo.xindazhou.core.log.ILog
+import kotlinx.coroutines.launch
+
+/**
+ * 帖子详情 Activity
+ * 
+ * 上方显示帖子详情,下方显示评论列表
+ */
+class PostDetailActivity : BaseActivity<ActivityPostDetailBinding>() {
+    
+    private val viewModel: PostDetailViewModel by viewModels {
+        CommunityViewModelFactory(application)
+    }
+    
+    private lateinit var commentAdapter: CommentAdapter
+    private var postId: Long = 0
+    
+    override fun getViewBinding(): ActivityPostDetailBinding {
+        return ActivityPostDetailBinding.inflate(layoutInflater)
+    }
+    
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        
+        postId = intent.getLongExtra("postId", 0)
+        if (postId == 0L) {
+            Toast.makeText(this, "帖子ID无效", Toast.LENGTH_SHORT).show()
+            finish()
+            return
+        }
+        
+        initView()
+        observeState()
+        
+        // 加载数据
+        viewModel.loadPostDetail(postId)
+    }
+    
+    override fun initView() {
+        // 初始化评论列表
+        commentAdapter = CommentAdapter(
+            onItemClick = { comment ->
+                // 点击评论,准备回复
+                binding.etComment.hint = "@${comment.userNickname}"
+                binding.etComment.requestFocus()
+                viewModel.setReplyToComment(comment.id, comment.userNickname)
+            },
+            onLikeClick = { comment ->
+                viewModel.toggleLikeComment(comment)
+            },
+            onReplyClick = { comment ->
+                // 显示回复输入框
+                binding.etComment.hint = "@${comment.userNickname}"
+                binding.etComment.requestFocus()
+                viewModel.setReplyToComment(comment.id, comment.userNickname)
+            }
+        )
+        
+        binding.rvComments.layoutManager = LinearLayoutManager(this)
+        binding.rvComments.adapter = commentAdapter
+        
+        // 发布评论
+        binding.btnPostComment.setOnClickListener {
+            val content = binding.etComment.text.toString().trim()
+            if (content.isEmpty()) {
+                Toast.makeText(this, "请输入评论内容", Toast.LENGTH_SHORT).show()
+                return@setOnClickListener
+            }
+            // 获取当前回复的parentId
+            val parentId = viewModel.replyToCommentId.value
+            viewModel.postComment(content, parentId)
+        }
+        
+        // 返回按钮
+        binding.ivBack.setOnClickListener {
+            finish()
+        }
+    }
+    
+    private fun observeState() {
+        // 观察帖子详情状态
+        lifecycleScope.launch {
+            viewModel.postDetailState.collect { state ->
+                when (state) {
+                    is PostDetailState.Idle -> {
+                        // 初始状态
+                    }
+                    is PostDetailState.Loading -> {
+                        showLoading()
+                    }
+                    is PostDetailState.Success -> {
+                        hideLoading()
+                        bindPost(state.post)
+                    }
+                    is PostDetailState.Error -> {
+                        hideLoading()
+                        showError(state.message)
+                    }
+                }
+            }
+        }
+        
+        // 观察评论列表状态
+        lifecycleScope.launch {
+            viewModel.commentState.collect { state ->
+                when (state) {
+                    is CommentState.Idle -> {
+                        // 初始状态
+                    }
+                    is CommentState.Loading -> {
+                        // 加载中
+                    }
+                    is CommentState.Success -> {
+                        commentAdapter.submitList(state.comments)
+                    }
+                    is CommentState.Error -> {
+                        showError(state.message)
+                    }
+                }
+            }
+        }
+        
+        // 观察发布评论状态
+        lifecycleScope.launch {
+            viewModel.postCommentState.collect { state ->
+                when (state) {
+                    is PostCommentState.Idle -> {
+                        // 初始状态
+                    }
+                    is PostCommentState.Loading -> {
+                        binding.btnPostComment.isEnabled = false
+                    }
+                    is PostCommentState.Success -> {
+                        binding.btnPostComment.isEnabled = true
+                        binding.etComment.setText("")
+                        binding.etComment.hint = "写评论..."
+                        // resetPostCommentState已经在postComment中调用了
+                        // 评论发布成功后,重新加载帖子详情以获取最新的评论数量
+                        // 然后通过 ActivityResult 返回更新的数据
+                        lifecycleScope.launch {
+                            // 等待帖子详情更新完成
+                            kotlinx.coroutines.delay(500)
+                            val currentState = viewModel.postDetailState.value
+                            if (currentState is PostDetailState.Success) {
+                                val resultIntent = Intent().apply {
+                                    putExtra("postId", postId)
+                                    putExtra("commentCount", currentState.post.commentCount)
+                                }
+                                setResult(RESULT_OK, resultIntent)
+                            }
+                        }
+                    }
+                    is PostCommentState.Error -> {
+                        binding.btnPostComment.isEnabled = true
+                        showError(state.message)
+                    }
+                }
+            }
+        }
+    }
+    
+    private fun bindPost(post: Post) {
+        // 用户信息
+        binding.tvNickname.text = post.userNickname ?: "匿名用户"
+        binding.tvTime.text = formatTime(post.createTime)
+        
+        // 关注按钮
+        updateFollowButton(post)
+        binding.tvFollow.setOnClickListener {
+            post.userId?.let { userId ->
+                viewModel.toggleFollowAuthor(userId)
+            }
+        }
+        
+        // 帖子内容
+        binding.tvTitle.text = post.title
+        binding.tvContent.text = post.content ?: ""
+        
+        // 互动数据
+        binding.tvLikeCount.text = post.likeCount.toString()
+        binding.tvCommentCount.text = post.commentCount.toString()
+        binding.tvCollectCount.text = post.collectCount.toString()
+        
+        // 互动按钮
+        binding.llLike.setOnClickListener {
+            viewModel.toggleLikePost(post)
+        }
+        binding.llCollect.setOnClickListener {
+            viewModel.toggleCollectPost(post)
+        }
+        binding.ivShare.setOnClickListener {
+            sharePost(post)
+        }
+        
+        // 更新点赞/收藏状态
+        updateInteractionState(post)
+    }
+    
+    private fun updateInteractionState(post: Post) {
+        // 点赞状态
+        val likeIcon = if (post.isLiked == true) {
+            android.R.drawable.star_big_on // TODO: 使用自定义图标
+        } else {
+            android.R.drawable.star_big_off // TODO: 使用自定义图标
+        }
+        binding.ivLike.setImageResource(likeIcon)
+        binding.ivLike.setColorFilter(
+            if (post.isLiked == true) {
+                getColor(com.narutohuo.xindazhou.R.color.accent_primary)
+            } else {
+                getColor(com.narutohuo.xindazhou.R.color.text_tertiary)
+            }
+        )
+        
+        // 收藏状态
+        val collectIcon = if (post.isCollected == true) {
+            android.R.drawable.star_big_on // TODO: 使用自定义图标
+        } else {
+            android.R.drawable.star_big_off // TODO: 使用自定义图标
+        }
+        binding.ivCollect.setImageResource(collectIcon)
+        binding.ivCollect.setColorFilter(
+            if (post.isCollected == true) {
+                getColor(com.narutohuo.xindazhou.R.color.accent_primary)
+            } else {
+                getColor(com.narutohuo.xindazhou.R.color.text_tertiary)
+            }
+        )
+    }
+    
+    /**
+     * 更新关注按钮状态
+     */
+    private fun updateFollowButton(post: Post) {
+        binding.tvFollow.text = if (post.isFollowed == true) "已关注" else "关注"
+    }
+    
+    /**
+     * 分享帖子
+     */
+    private fun sharePost(post: Post) {
+        try {
+            val shareService = ShareKit.getInstance()
+            
+            // 构建分享链接(TODO: 使用实际的帖子详情页链接)
+            val postUrl = "https://www.xindazhou.com/community/post/${post.id}"
+            
+            // 创建分享内容
+            val shareContent = ShareContent.builder()
+                .setTitle(post.title)
+                .setDescription(post.content ?: "")
+                .setUrl(postUrl)
+                .setImageUrl(post.mediaList?.firstOrNull()?.mediaUrl) // 使用第一张图片作为分享图片
+                .build()
+            
+            // 调用分享(不指定平台,会显示分享弹窗)
+            // ShareProxyActivity 会自动处理回调并关闭,回调已经在主线程
+            shareService.share(this, shareContent) { response ->
+                if (response.success) {
+                    val platformName = response.data?.name ?: "未知平台"
+                    Toast.makeText(this@PostDetailActivity, "分享成功:$platformName", Toast.LENGTH_SHORT).show()
+                } else {
+                    // 用户取消分享时不显示 Toast
+                    if (response.errorMessage != "用户取消分享") {
+                        val errorMsg = response.errorMessage ?: "分享失败"
+                        Toast.makeText(this@PostDetailActivity, errorMsg, Toast.LENGTH_SHORT).show()
+                    }
+                }
+            }
+        } catch (e: Exception) {
+            ILog.e("PostDetailActivity", "分享功能异常", e)
+            Toast.makeText(this, "分享功能异常:${e.message}", Toast.LENGTH_SHORT).show()
+        }
+    }
+    
+    private fun formatTime(timestamp: Long?): String {
+        if (timestamp == null) return ""
+        val now = System.currentTimeMillis()
+        val diff = now - timestamp
+        
+        return when {
+            diff < 60_000 -> "刚刚"
+            diff < 3600_000 -> "${diff / 60_000}分钟前"
+            diff < 86400_000 -> "${diff / 3600_000}小时前"
+            diff < 604800_000 -> "${diff / 86400_000}天前"
+            else -> {
+                val sdf = java.text.SimpleDateFormat("MM-dd", java.util.Locale.getDefault())
+                sdf.format(java.util.Date(timestamp))
+            }
+        }
+    }
+}
+

+ 206 - 0
app/src/main/java/com/narutohuo/xindazhou/community/ui/dialog/CityPickerDialog.kt

@@ -0,0 +1,206 @@
+package com.narutohuo.xindazhou.community.ui.dialog
+
+import android.app.Dialog
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.fragment.app.DialogFragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.narutohuo.xindazhou.databinding.DialogCityPickerBinding
+
+/**
+ * 城市选择对话框
+ * 参考微信/美团方案:热门城市 + 全部城市列表
+ */
+class CityPickerDialog : DialogFragment() {
+
+    private var _binding: DialogCityPickerBinding? = null
+    private val binding get() = _binding!!
+
+    private var onCitySelected: ((String) -> Unit)? = null
+
+    // 热门城市列表
+    private val hotCities = listOf(
+        "北京", "上海", "广州", "深圳", "杭州", "成都", "武汉", "西安",
+        "南京", "重庆", "天津", "苏州", "长沙", "郑州", "东莞", "青岛"
+    )
+
+    // 全部城市列表(按省份分组,简化版)
+    private val allCities = mapOf(
+        "直辖市" to listOf("北京", "上海", "天津", "重庆"),
+        "广东" to listOf("广州", "深圳", "东莞", "佛山", "中山", "珠海", "惠州"),
+        "江苏" to listOf("南京", "苏州", "无锡", "常州", "南通", "扬州"),
+        "浙江" to listOf("杭州", "宁波", "温州", "嘉兴", "湖州", "绍兴"),
+        "山东" to listOf("济南", "青岛", "烟台", "潍坊", "临沂"),
+        "四川" to listOf("成都", "绵阳", "德阳", "南充", "宜宾"),
+        "湖北" to listOf("武汉", "襄阳", "宜昌", "荆州", "黄冈"),
+        "陕西" to listOf("西安", "宝鸡", "咸阳", "渭南", "汉中"),
+        "河南" to listOf("郑州", "洛阳", "新乡", "南阳", "开封"),
+        "湖南" to listOf("长沙", "株洲", "湘潭", "衡阳", "岳阳")
+    )
+
+    companion object {
+        fun newInstance(onCitySelected: (String) -> Unit): CityPickerDialog {
+            return CityPickerDialog().apply {
+                this.onCitySelected = onCitySelected
+            }
+        }
+    }
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        _binding = DialogCityPickerBinding.inflate(LayoutInflater.from(requireContext()))
+        
+        val dialog = MaterialAlertDialogBuilder(requireContext())
+            .setTitle("选择城市")
+            .setView(binding.root)
+            .create()
+
+        initView()
+        return dialog
+    }
+
+    private fun initView() {
+        // 热门城市
+        binding.rvHotCities.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
+        binding.rvHotCities.adapter = HotCityAdapter(hotCities) { city ->
+            onCitySelected?.invoke(city)
+            dismiss()
+        }
+
+        // 全部城市
+        binding.rvAllCities.layoutManager = LinearLayoutManager(requireContext())
+        binding.rvAllCities.adapter = AllCityAdapter(allCities) { city ->
+            onCitySelected?.invoke(city)
+            dismiss()
+        }
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        _binding = null
+    }
+
+    // 热门城市Adapter
+    private class HotCityAdapter(
+        private val cities: List<String>,
+        private val onItemClick: (String) -> Unit
+    ) : RecyclerView.Adapter<HotCityAdapter.ViewHolder>() {
+
+        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+            val view = TextView(parent.context).apply {
+                layoutParams = ViewGroup.MarginLayoutParams(
+                    ViewGroup.LayoutParams.WRAP_CONTENT,
+                    ViewGroup.LayoutParams.WRAP_CONTENT
+                ).apply {
+                    setMargins(8, 8, 8, 8)
+                }
+                setPadding(24, 16, 24, 16)
+                setBackgroundResource(com.narutohuo.xindazhou.R.drawable.bg_tab_unselected)
+                textSize = 14f
+            }
+            return ViewHolder(view)
+        }
+
+        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+            holder.bind(cities[position])
+        }
+
+        override fun getItemCount() = cities.size
+
+        inner class ViewHolder(private val textView: TextView) : RecyclerView.ViewHolder(textView) {
+            fun bind(city: String) {
+                textView.text = city
+                textView.setOnClickListener { onItemClick(city) }
+            }
+        }
+    }
+
+    // 全部城市Adapter
+    private class AllCityAdapter(
+        private val cityMap: Map<String, List<String>>,
+        private val onItemClick: (String) -> Unit
+    ) : RecyclerView.Adapter<AllCityAdapter.ViewHolder>() {
+
+        private val items = mutableListOf<Item>()
+
+        init {
+            cityMap.forEach { (province, cities) ->
+                items.add(Item.Header(province))
+                cities.forEach { city ->
+                    items.add(Item.City(city))
+                }
+            }
+        }
+
+        override fun getItemViewType(position: Int): Int {
+            return when (items[position]) {
+                is Item.Header -> 0
+                is Item.City -> 1
+            }
+        }
+
+        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+            return when (viewType) {
+                0 -> {
+                    val view = TextView(parent.context).apply {
+                        layoutParams = ViewGroup.LayoutParams(
+                            ViewGroup.LayoutParams.MATCH_PARENT,
+                            ViewGroup.LayoutParams.WRAP_CONTENT
+                        )
+                        setPadding(16, 12, 16, 12)
+                        textSize = 16f
+                        setTypeface(null, android.graphics.Typeface.BOLD)
+                        setBackgroundColor(0xFFF5F5F5.toInt())
+                    }
+                    ViewHolder.Header(view)
+                }
+                else -> {
+                    val view = TextView(parent.context).apply {
+                        layoutParams = ViewGroup.LayoutParams(
+                            ViewGroup.LayoutParams.MATCH_PARENT,
+                            ViewGroup.LayoutParams.WRAP_CONTENT
+                        )
+                        setPadding(32, 16, 16, 16)
+                        textSize = 14f
+                        setBackgroundResource(android.R.drawable.list_selector_background)
+                    }
+                    ViewHolder.City(view, onItemClick)
+                }
+            }
+        }
+
+        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+            when (val item = items[position]) {
+                is Item.Header -> (holder as ViewHolder.Header).bind(item.province)
+                is Item.City -> (holder as ViewHolder.City).bind(item.city)
+            }
+        }
+
+        override fun getItemCount() = items.size
+
+        sealed class Item {
+            data class Header(val province: String) : Item()
+            data class City(val city: String) : Item()
+        }
+
+        sealed class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+            class Header(private val textView: TextView) : ViewHolder(textView) {
+                fun bind(province: String) {
+                    textView.text = province
+                }
+            }
+
+            class City(private val textView: TextView, private val onCityClick: (String) -> Unit) : ViewHolder(textView) {
+                fun bind(city: String) {
+                    textView.text = city
+                    textView.setOnClickListener { onCityClick(city) }
+                }
+            }
+        }
+    }
+}
+

+ 27 - 0
app/src/main/java/com/narutohuo/xindazhou/community/ui/viewmodel/CommentState.kt

@@ -0,0 +1,27 @@
+package com.narutohuo.xindazhou.community.ui.viewmodel
+
+import com.narutohuo.xindazhou.community.model.Comment
+
+/**
+ * 评论列表状态
+ */
+sealed class CommentState {
+    object Idle : CommentState()
+    object Loading : CommentState()
+    data class Success(
+        val comments: List<Comment>,
+        val hasMore: Boolean
+    ) : CommentState()
+    data class Error(val message: String) : CommentState()
+}
+
+/**
+ * 发布评论状态
+ */
+sealed class PostCommentState {
+    object Idle : PostCommentState()
+    object Loading : PostCommentState()
+    data class Success(val commentId: Long) : PostCommentState()
+    data class Error(val message: String) : PostCommentState()
+}
+

+ 29 - 0
app/src/main/java/com/narutohuo/xindazhou/community/ui/viewmodel/CommunityState.kt

@@ -0,0 +1,29 @@
+package com.narutohuo.xindazhou.community.ui.viewmodel
+
+import com.narutohuo.xindazhou.community.model.Banner
+import com.narutohuo.xindazhou.community.model.Post
+
+/**
+ * 社区列表状态
+ */
+sealed class CommunityState {
+    object Idle : CommunityState()
+    object Loading : CommunityState()
+    data class Success(
+        val posts: List<Post>,
+        val banners: List<Banner>,
+        val hasMore: Boolean
+    ) : CommunityState()
+    data class Error(val message: String) : CommunityState()
+}
+
+/**
+ * 帖子详情状态
+ */
+sealed class PostDetailState {
+    object Idle : PostDetailState()
+    object Loading : PostDetailState()
+    data class Success(val post: Post) : PostDetailState()
+    data class Error(val message: String) : PostDetailState()
+}
+

+ 304 - 0
app/src/main/java/com/narutohuo/xindazhou/community/ui/viewmodel/CommunityViewModel.kt

@@ -0,0 +1,304 @@
+package com.narutohuo.xindazhou.community.ui.viewmodel
+
+import android.app.Application
+import androidx.lifecycle.viewModelScope
+import com.narutohuo.xindazhou.community.model.Banner
+import com.narutohuo.xindazhou.community.model.Post
+import com.narutohuo.xindazhou.community.model.Topic
+import com.narutohuo.xindazhou.community.repository.CommunityRepository
+import com.narutohuo.xindazhou.common.network.response.ApiResponse
+import com.narutohuo.xindazhou.common.network.response.onSuccess
+import com.narutohuo.xindazhou.common.network.response.onError
+import com.narutohuo.xindazhou.common.network.response.onException
+import com.narutohuo.xindazhou.common.ui.BaseViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+/**
+ * 社区列表 ViewModel
+ */
+class CommunityViewModel(
+    application: Application,
+    private val repository: CommunityRepository
+) : BaseViewModel(application) {
+    
+    private val _communityState = MutableStateFlow<CommunityState>(CommunityState.Idle)
+    val communityState: StateFlow<CommunityState> = _communityState
+    
+    private val _topicsState = MutableStateFlow<List<Topic>>(emptyList())
+    val topicsState: StateFlow<List<Topic>> = _topicsState
+    
+    private val _currentCityState = MutableStateFlow<String?>(null)
+    val currentCity: StateFlow<String?> = _currentCityState
+    
+    private var currentPage = 1
+    private val pageSize = 10
+    private var currentCategory: String? = null
+    private var currentKeyword: String? = null
+    private var currentSort: String? = null
+    private var currentCityFilter: String? = null
+    private var currentTopicId: Long? = null
+    
+    private val posts = mutableListOf<Post>()
+    private var banners = listOf<Banner>()
+    private var hasMore = true
+    
+    init {
+        loadBanners()
+        loadTopics()
+    }
+    
+    /**
+     * 加载 Banner 列表
+     */
+    private fun loadBanners() {
+        viewModelScope.launch {
+            repository.getBannerList()
+                .onSuccess { bannerList ->
+                    banners = bannerList
+                    updateState()
+                }
+                .onError { _, message ->
+                    // Banner 加载失败不影响帖子列表
+                }
+                .onException {
+                    // Banner 加载失败不影响帖子列表
+                }
+        }
+    }
+    
+    /**
+     * 加载话题列表
+     */
+    private fun loadTopics() {
+        viewModelScope.launch {
+            repository.getTopicList()
+                .onSuccess { topicList ->
+                    _topicsState.value = topicList
+                }
+                .onError { _, _ ->
+                    // 话题加载失败不影响帖子列表
+                }
+                .onException {
+                    // 话题加载失败不影响帖子列表
+                }
+        }
+    }
+    
+    /**
+     * 加载帖子列表(首次加载或刷新)
+     */
+    fun loadPosts(
+        category: String? = null,
+        keyword: String? = null,
+        sort: String? = null,
+        city: String? = null,
+        topicId: Long? = null,
+        refresh: Boolean = false
+    ) {
+        if (refresh) {
+            currentPage = 1
+            posts.clear()
+            hasMore = true
+        }
+        
+        currentCategory = category
+        currentKeyword = keyword
+        currentSort = sort
+        currentCityFilter = city
+        currentTopicId = topicId
+        
+        if (!hasMore && !refresh) {
+            return
+        }
+        
+        viewModelScope.launch {
+            try {
+                _communityState.value = CommunityState.Loading
+                val response = repository.getPostPage(currentPage, pageSize, category, keyword, sort, city, topicId)
+                response.onSuccess { pageResult ->
+                    if (refresh) {
+                        posts.clear()
+                    }
+                    posts.addAll(pageResult.list)
+                    hasMore = posts.size < pageResult.total
+                    currentPage++
+                    _communityState.value = CommunityState.Success(posts.toList(), banners, hasMore)
+                }.onError { _, message ->
+                    _communityState.value = CommunityState.Error(message)
+                }.onException { exception ->
+                    _communityState.value = CommunityState.Error(exception.message ?: "加载失败")
+                }
+            } catch (e: Exception) {
+                _communityState.value = CommunityState.Error(handleError(e, "加载失败"))
+            }
+        }
+    }
+    
+    /**
+     * 加载更多
+     */
+    fun loadMore() {
+        if (!hasMore) {
+            return
+        }
+        loadPosts(currentCategory, currentKeyword, currentSort, currentCityFilter, currentTopicId, refresh = false)
+    }
+    
+    /**
+     * 刷新
+     */
+    fun refresh() {
+        loadPosts(currentCategory, currentKeyword, currentSort, currentCityFilter, currentTopicId, refresh = true)
+    }
+    
+    /**
+     * 切换分类
+     */
+    fun switchCategory(category: String?) {
+        loadPosts(category, currentKeyword, currentSort, currentCityFilter, currentTopicId, refresh = true)
+    }
+    
+    /**
+     * 搜索
+     */
+    fun search(keyword: String?) {
+        loadPosts(currentCategory, keyword, currentSort, currentCityFilter, currentTopicId, refresh = true)
+    }
+    
+    /**
+     * 切换排序
+     */
+    fun switchSort(sort: String?) {
+        loadPosts(currentCategory, currentKeyword, sort, currentCityFilter, currentTopicId, refresh = true)
+    }
+    
+    /**
+     * 设置城市
+     */
+    fun setCity(city: String?) {
+        _currentCityState.value = city
+        currentCityFilter = city
+        loadPosts(currentCategory, currentKeyword, currentSort, city, currentTopicId, refresh = true)
+    }
+    
+    /**
+     * 选择话题
+     */
+    fun selectTopic(topicId: Long?) {
+        currentTopicId = topicId
+        loadPosts(currentCategory, currentKeyword, currentSort, currentCityFilter, topicId, refresh = true)
+    }
+    
+    /**
+     * 更新状态
+     */
+    private fun updateState() {
+        if (_communityState.value is CommunityState.Success) {
+            _communityState.value = CommunityState.Success(posts.toList(), banners, hasMore)
+        }
+    }
+    
+    /**
+     * 点赞/取消点赞
+     */
+    fun toggleLike(post: Post) {
+        viewModelScope.launch {
+            repository.toggleLike(1, post.id)
+                .onSuccess {
+                    // 更新本地数据
+                    val index = posts.indexOfFirst { it.id == post.id }
+                    if (index >= 0) {
+                        val currentLiked = post.isLiked ?: false
+                        val updatedPost = post.copy(
+                            isLiked = !currentLiked,
+                            likeCount = if (currentLiked) {
+                                post.likeCount - 1
+                            } else {
+                                post.likeCount + 1
+                            }
+                        )
+                        posts[index] = updatedPost
+                        updateState()
+                    }
+                }
+        }
+    }
+    
+    /**
+     * 收藏/取消收藏
+     */
+    fun toggleCollect(post: Post) {
+        viewModelScope.launch {
+            repository.toggleCollect(post.id)
+                .onSuccess {
+                    // 更新本地数据
+                    val index = posts.indexOfFirst { it.id == post.id }
+                    if (index >= 0) {
+                        val currentCollected = post.isCollected ?: false
+                        val updatedPost = post.copy(
+                            isCollected = !currentCollected,
+                            collectCount = if (currentCollected) {
+                                post.collectCount - 1
+                            } else {
+                                post.collectCount + 1
+                            }
+                        )
+                        posts[index] = updatedPost
+                        updateState()
+                    }
+                }
+        }
+    }
+    
+    /**
+     * 关注/取消关注
+     */
+    fun toggleFollow(post: Post) {
+        viewModelScope.launch {
+            repository.toggleFollow(post.userId)
+                .onSuccess {
+                    // 更新本地数据
+                    val index = posts.indexOfFirst { it.id == post.id }
+                    if (index >= 0) {
+                        val currentFollowed = post.isFollowed ?: false
+                        val updatedPost = post.copy(
+                            isFollowed = !currentFollowed
+                        )
+                        posts[index] = updatedPost
+                        updateState()
+                    }
+                }
+        }
+    }
+    
+    /**
+     * 更新指定帖子的评论数量(用于从帖子详情页返回时更新)
+     */
+    fun updatePostCommentCount(postId: Long, commentCount: Int) {
+        val index = posts.indexOfFirst { it.id == postId }
+        if (index >= 0) {
+            val post = posts[index]
+            posts[index] = post.copy(commentCount = commentCount)
+            updateState()
+        }
+    }
+    
+    /**
+     * 刷新指定帖子(用于从帖子详情页返回时,重新获取最新数据)
+     */
+    fun refreshPost(postId: Long) {
+        viewModelScope.launch {
+            repository.getPost(postId)
+                .onSuccess { updatedPost ->
+                    val index = posts.indexOfFirst { it.id == postId }
+                    if (index >= 0) {
+                        posts[index] = updatedPost
+                        updateState()
+                    }
+                }
+        }
+    }
+}
+

+ 32 - 0
app/src/main/java/com/narutohuo/xindazhou/community/ui/viewmodel/CommunityViewModelFactory.kt

@@ -0,0 +1,32 @@
+package com.narutohuo.xindazhou.community.ui.viewmodel
+
+import android.app.Application
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.narutohuo.xindazhou.community.datasource.remote.CommunityRemoteDataSourceImpl
+import com.narutohuo.xindazhou.community.repository.CommunityRepository
+
+/**
+ * CommunityViewModel Factory
+ */
+class CommunityViewModelFactory(
+    private val application: Application
+) : ViewModelProvider.Factory {
+    
+    @Suppress("UNCHECKED_CAST")
+    override fun <T : ViewModel> create(modelClass: Class<T>): T {
+        val repository = CommunityRepository(CommunityRemoteDataSourceImpl())
+        
+        if (modelClass.isAssignableFrom(CommunityViewModel::class.java)) {
+            return CommunityViewModel(application, repository) as T
+        }
+        if (modelClass.isAssignableFrom(PostDetailViewModel::class.java)) {
+            return PostDetailViewModel(application, repository) as T
+        }
+        if (modelClass.isAssignableFrom(CreatePostViewModel::class.java)) {
+            return CreatePostViewModel(application, repository) as T
+        }
+        throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
+    }
+}
+

+ 26 - 0
app/src/main/java/com/narutohuo/xindazhou/community/ui/viewmodel/CreatePostState.kt

@@ -0,0 +1,26 @@
+package com.narutohuo.xindazhou.community.ui.viewmodel
+
+/**
+ * 发帖状态
+ */
+sealed class CreatePostState {
+    object Idle : CreatePostState()
+    object Loading : CreatePostState()
+    data class Success(val postId: Long) : CreatePostState()
+    data class Error(val message: String) : CreatePostState()
+}
+
+/**
+ * 媒体项(用于显示已选择的图片/视频)
+ */
+data class MediaItem(
+    val uri: android.net.Uri,
+    val type: MediaType,
+    val thumbnailUri: android.net.Uri? = null
+) {
+    enum class MediaType {
+        IMAGE,
+        VIDEO
+    }
+}
+

+ 167 - 0
app/src/main/java/com/narutohuo/xindazhou/community/ui/viewmodel/CreatePostViewModel.kt

@@ -0,0 +1,167 @@
+package com.narutohuo.xindazhou.community.ui.viewmodel
+
+import android.app.Application
+import android.net.Uri
+import androidx.lifecycle.viewModelScope
+import com.narutohuo.xindazhou.community.datasource.remote.CommunityApi
+import com.narutohuo.xindazhou.community.repository.CommunityRepository
+import com.narutohuo.xindazhou.common.network.response.ApiResponse
+import com.narutohuo.xindazhou.common.network.response.onSuccess
+import com.narutohuo.xindazhou.common.network.response.onError
+import com.narutohuo.xindazhou.common.network.response.onException
+import com.narutohuo.xindazhou.common.ui.BaseViewModel
+import com.narutohuo.xindazhou.core.log.ILog
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+/**
+ * 发帖 ViewModel
+ */
+class CreatePostViewModel(
+    application: Application,
+    private val repository: CommunityRepository
+) : BaseViewModel(application) {
+    
+    private val _createPostState = MutableStateFlow<CreatePostState>(CreatePostState.Idle)
+    val createPostState: StateFlow<CreatePostState> = _createPostState
+    
+    // 封面图片 URI
+    var coverImageUri: Uri? = null
+        private set
+    
+    // 媒体列表
+    private val mediaItems = mutableListOf<MediaItem>()
+    
+    /**
+     * 设置封面图片
+     */
+    fun setCoverImage(uri: Uri) {
+        coverImageUri = uri
+    }
+    
+    /**
+     * 清除封面图片
+     */
+    fun clearCoverImage() {
+        coverImageUri = null
+    }
+    
+    /**
+     * 添加图片
+     */
+    fun addImages(uris: List<Uri>) {
+        val currentImageCount = mediaItems.count { it.type == MediaItem.MediaType.IMAGE }
+        val remainingSlots = 9 - currentImageCount
+        
+        if (remainingSlots <= 0) {
+            _createPostState.value = CreatePostState.Error("图片最多只能添加9张")
+            return
+        }
+        
+        val urisToAdd = uris.take(remainingSlots)
+        urisToAdd.forEach { uri ->
+            mediaItems.add(MediaItem(uri, MediaItem.MediaType.IMAGE))
+        }
+        
+        if (uris.size > remainingSlots) {
+            _createPostState.value = CreatePostState.Error("图片最多只能添加9张,已自动截取前${remainingSlots}张")
+        }
+    }
+    
+    /**
+     * 添加视频
+     */
+    fun addVideo(uri: Uri) {
+        // 检查是否已有视频
+        if (mediaItems.any { it.type == MediaItem.MediaType.VIDEO }) {
+            _createPostState.value = CreatePostState.Error("视频最多只能添加1个")
+            return
+        }
+        
+        mediaItems.add(MediaItem(uri, MediaItem.MediaType.VIDEO))
+    }
+    
+    /**
+     * 删除媒体
+     */
+    fun removeMedia(index: Int) {
+        if (index in mediaItems.indices) {
+            mediaItems.removeAt(index)
+        }
+    }
+    
+    /**
+     * 获取媒体列表
+     */
+    fun getMediaItems(): List<MediaItem> = mediaItems.toList()
+    
+    /**
+     * 发布帖子
+     * 
+     * 注意:这里假设媒体文件已经上传到服务器并获得了 URL
+     * 实际实现中需要先上传文件,获取 URL 后再提交帖子
+     */
+    fun publishPost(
+        title: String,
+        content: String?,
+        tags: String? = null
+    ) {
+        if (title.isBlank()) {
+            _createPostState.value = CreatePostState.Error("请输入标题")
+            return
+        }
+        
+        viewModelScope.launch {
+            try {
+                _createPostState.value = CreatePostState.Loading
+                
+                // TODO: 先上传媒体文件,获取 URL
+                // 这里暂时使用占位符,实际需要调用文件上传 API
+                val mediaList = mediaItems.mapIndexed { index, item ->
+                    CommunityApi.PostMediaRequest(
+                        mediaType = if (item.type == MediaItem.MediaType.IMAGE) 1 else 2,
+                        mediaUrl = item.uri.toString(), // TODO: 替换为实际上传后的 URL
+                        coverUrl = null,
+                        duration = null,
+                        fileSize = null,
+                        sort = index
+                    )
+                }
+                
+                // 构建请求
+                val request = CommunityApi.CreatePostRequest(
+                    title = title,
+                    content = content,
+                    contentType = when {
+                        mediaItems.isEmpty() -> 1 // 纯文本
+                        mediaItems.any { it.type == MediaItem.MediaType.VIDEO } -> 3 // 视频
+                        mediaItems.any { it.type == MediaItem.MediaType.IMAGE } -> 2 // 图片
+                        else -> 1
+                    },
+                    mediaList = if (mediaList.isEmpty()) null else mediaList,
+                    tags = tags
+                )
+                
+                // 提交帖子
+                ILog.d("CreatePostViewModel", "开始发布帖子,请求参数:title=$title, contentType=${request.contentType}, mediaCount=${mediaList.size}")
+                repository.createPost(request)
+                    .onSuccess { postId ->
+                        ILog.d("CreatePostViewModel", "发布帖子成功,postId=$postId")
+                        _createPostState.value = CreatePostState.Success(postId)
+                    }
+                    .onError { code, message ->
+                        ILog.e("CreatePostViewModel", "发布帖子失败,code=$code, message=$message")
+                        _createPostState.value = CreatePostState.Error(message)
+                    }
+                    .onException { exception ->
+                        ILog.e("CreatePostViewModel", "发布帖子异常", exception)
+                        _createPostState.value = CreatePostState.Error(exception.message ?: "发布失败")
+                    }
+            } catch (e: Exception) {
+                _createPostState.value = CreatePostState.Error(handleError(e, "发布失败"))
+            }
+        }
+    }
+}
+

+ 276 - 0
app/src/main/java/com/narutohuo/xindazhou/community/ui/viewmodel/PostDetailViewModel.kt

@@ -0,0 +1,276 @@
+package com.narutohuo.xindazhou.community.ui.viewmodel
+
+import android.app.Application
+import androidx.lifecycle.viewModelScope
+import com.narutohuo.xindazhou.community.datasource.remote.CommunityApi.CreateCommentRequest
+import com.narutohuo.xindazhou.community.model.Comment
+import com.narutohuo.xindazhou.community.model.Post
+import com.narutohuo.xindazhou.community.repository.CommunityRepository
+import com.narutohuo.xindazhou.common.network.response.ApiResponse
+import com.narutohuo.xindazhou.common.network.response.onSuccess
+import com.narutohuo.xindazhou.common.network.response.onError
+import com.narutohuo.xindazhou.common.network.response.onException
+import com.narutohuo.xindazhou.common.ui.BaseViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+/**
+ * 帖子详情 ViewModel
+ */
+class PostDetailViewModel(
+    application: Application,
+    private val repository: CommunityRepository
+) : BaseViewModel(application) {
+    
+    private val _postDetailState = MutableStateFlow<PostDetailState>(PostDetailState.Idle)
+    val postDetailState: StateFlow<PostDetailState> = _postDetailState
+    
+    private val _commentState = MutableStateFlow<CommentState>(CommentState.Idle)
+    val commentState: StateFlow<CommentState> = _commentState
+    
+    private val _postCommentState = MutableStateFlow<PostCommentState>(PostCommentState.Idle)
+    val postCommentState: StateFlow<PostCommentState> = _postCommentState
+    
+    private val _replyToCommentId = MutableStateFlow<Long?>(null)
+    val replyToCommentId: StateFlow<Long?> = _replyToCommentId
+    
+    private val _replyToUserNickname = MutableStateFlow<String?>(null)
+    val replyToUserNickname: StateFlow<String?> = _replyToUserNickname
+    
+    private var currentPostId: Long? = null
+    private var currentPage = 1
+    private val pageSize = 20
+    
+    private val comments = mutableListOf<Comment>()
+    private var hasMore = true
+    
+    /**
+     * 加载帖子详情
+     */
+    fun loadPostDetail(postId: Long) {
+        currentPostId = postId
+        viewModelScope.launch {
+            try {
+                _postDetailState.value = PostDetailState.Loading
+                val response = repository.getPost(postId)
+                response.onSuccess { post ->
+                    _postDetailState.value = PostDetailState.Success(post)
+                }.onError { _, message ->
+                    _postDetailState.value = PostDetailState.Error(message)
+                }.onException { exception ->
+                    _postDetailState.value = PostDetailState.Error(exception.message ?: "加载失败")
+                }
+            } catch (e: Exception) {
+                _postDetailState.value = PostDetailState.Error(handleError(e, "加载失败"))
+            }
+        }
+        
+        // 同时加载评论列表
+        loadComments(postId, refresh = true)
+    }
+    
+    /**
+     * 加载评论列表
+     */
+    fun loadComments(postId: Long? = null, refresh: Boolean = false) {
+        val targetPostId = postId ?: currentPostId ?: return
+        
+        if (refresh) {
+            currentPage = 1
+            comments.clear()
+            hasMore = true
+        }
+        
+        if (!hasMore && !refresh) {
+            return
+        }
+        
+                viewModelScope.launch {
+                    try {
+                        _commentState.value = CommentState.Loading
+                        // 使用getCommentTree获取树形结构的评论
+                        val response = repository.getCommentTree(targetPostId)
+                        response.onSuccess { commentTree ->
+                            if (refresh) {
+                                comments.clear()
+                            }
+                            // 将树形结构展开为平铺列表(只显示顶级评论,回复在replies中)
+                            comments.clear()
+                            comments.addAll(commentTree)
+                            hasMore = false // 树形结构不需要分页
+                            _commentState.value = CommentState.Success(comments.toList(), hasMore)
+                        }.onError { _, message ->
+                            _commentState.value = CommentState.Error(message)
+                        }.onException { exception ->
+                            _commentState.value = CommentState.Error(exception.message ?: "加载评论失败")
+                        }
+                    } catch (e: Exception) {
+                        _commentState.value = CommentState.Error(handleError(e, "加载评论失败"))
+                    }
+                }
+    }
+    
+    /**
+     * 加载更多评论
+     */
+    fun loadMoreComments() {
+        if (!hasMore) {
+            return
+        }
+        loadComments(refresh = false)
+    }
+    
+    /**
+     * 设置回复目标评论
+     */
+    fun setReplyToComment(commentId: Long?, userNickname: String?) {
+        _replyToCommentId.value = commentId
+        _replyToUserNickname.value = userNickname
+    }
+    
+    /**
+     * 发布评论
+     */
+    fun postComment(content: String, parentId: Long? = null) {
+        val postId = currentPostId ?: return
+        // 如果传入了parentId,使用传入的;否则使用当前设置的replyToCommentId
+        val finalParentId = parentId ?: _replyToCommentId.value
+        
+        viewModelScope.launch {
+            try {
+                _postCommentState.value = PostCommentState.Loading
+                val response = repository.createComment(
+                    CreateCommentRequest(postId, finalParentId, content)
+                )
+                response.onSuccess { commentId ->
+                    _postCommentState.value = PostCommentState.Success(commentId)
+                    // 清除回复状态
+                    resetPostCommentState()
+                    // 重新加载评论列表和帖子详情(获取最新的评论数量)
+                    loadComments(refresh = true)
+                    currentPostId?.let { loadPostDetail(it) }
+                }.onError { _, message ->
+                    _postCommentState.value = PostCommentState.Error(message)
+                }.onException { exception ->
+                    _postCommentState.value = PostCommentState.Error(exception.message ?: "发布评论失败")
+                }
+            } catch (e: Exception) {
+                _postCommentState.value = PostCommentState.Error(handleError(e, "发布评论失败"))
+            }
+        }
+    }
+    
+    /**
+     * 删除评论
+     */
+    fun deleteComment(commentId: Long) {
+        viewModelScope.launch {
+            repository.deleteComment(commentId)
+                .onSuccess {
+                    // 从列表中移除
+                    comments.removeAll { it.id == commentId }
+                    _commentState.value = CommentState.Success(comments.toList(), hasMore)
+                }
+        }
+    }
+    
+    /**
+     * 点赞/取消点赞帖子
+     */
+    fun toggleLikePost(post: Post) {
+        viewModelScope.launch {
+            val response = repository.toggleLike(1, post.id)
+            response.onSuccess {
+                // 更新帖子详情
+                val currentLiked = post.isLiked ?: false
+                val updatedPost = post.copy(
+                    isLiked = !currentLiked,
+                    likeCount = if (currentLiked) {
+                        post.likeCount - 1
+                    } else {
+                        post.likeCount + 1
+                    }
+                )
+                _postDetailState.value = PostDetailState.Success(updatedPost)
+            }
+        }
+    }
+    
+    /**
+     * 收藏/取消收藏帖子
+     */
+    fun toggleCollectPost(post: Post) {
+        viewModelScope.launch {
+            val response = repository.toggleCollect(post.id)
+            response.onSuccess {
+                // 更新帖子详情
+                val currentCollected = post.isCollected ?: false
+                val updatedPost = post.copy(
+                    isCollected = !currentCollected,
+                    collectCount = if (currentCollected) {
+                        post.collectCount - 1
+                    } else {
+                        post.collectCount + 1
+                    }
+                )
+                _postDetailState.value = PostDetailState.Success(updatedPost)
+            }
+        }
+    }
+    
+    /**
+     * 关注/取消关注作者
+     */
+    fun toggleFollowAuthor(userId: Long) {
+        viewModelScope.launch {
+            val response = repository.toggleFollow(userId)
+            response.onSuccess {
+                // 更新帖子详情中的关注状态
+                val currentState = _postDetailState.value
+                if (currentState is PostDetailState.Success) {
+                    val updatedPost = currentState.post.copy(
+                        isFollowed = !(currentState.post.isFollowed ?: false)
+                    )
+                    _postDetailState.value = PostDetailState.Success(updatedPost)
+                }
+            }
+        }
+    }
+    
+    /**
+     * 点赞/取消点赞评论
+     */
+    fun toggleLikeComment(comment: Comment) {
+        viewModelScope.launch {
+            val response = repository.toggleLike(2, comment.id)
+            response.onSuccess {
+                // 更新本地数据
+                val index = comments.indexOfFirst { it.id == comment.id }
+                if (index >= 0) {
+                    val currentLiked = comment.isLiked ?: false
+                    val updatedComment = comment.copy(
+                        isLiked = !currentLiked,
+                        likeCount = if (currentLiked) {
+                            comment.likeCount - 1
+                        } else {
+                            comment.likeCount + 1
+                        }
+                    )
+                    comments[index] = updatedComment
+                    _commentState.value = CommentState.Success(comments.toList(), hasMore)
+                }
+            }
+        }
+    }
+    
+    /**
+     * 重置发布评论状态
+     */
+    fun resetPostCommentState() {
+        _postCommentState.value = PostCommentState.Idle
+        _replyToCommentId.value = null
+        _replyToUserNickname.value = null
+    }
+}
+

+ 67 - 0
app/src/main/java/com/narutohuo/xindazhou/community/util/CityCacheHelper.kt

@@ -0,0 +1,67 @@
+package com.narutohuo.xindazhou.community.util
+
+import android.content.Context
+import android.content.SharedPreferences
+
+/**
+ * 城市缓存工具类
+ */
+class CityCacheHelper(private val context: Context) {
+
+    private val prefs: SharedPreferences by lazy {
+        context.getSharedPreferences("community_city_cache", Context.MODE_PRIVATE)
+    }
+
+    private val KEY_CURRENT_CITY = "current_city"
+    private val KEY_LOCATION_CITY = "location_city"
+    private val KEY_LOCATION_TIME = "location_time"
+    private val CACHE_DURATION = 24 * 60 * 60 * 1000L // 24小时
+
+    /**
+     * 保存当前选择的城市
+     */
+    fun saveCurrentCity(city: String) {
+        prefs.edit().putString(KEY_CURRENT_CITY, city).apply()
+    }
+
+    /**
+     * 获取当前选择的城市
+     */
+    fun getCurrentCity(): String? {
+        return prefs.getString(KEY_CURRENT_CITY, null)
+    }
+
+    /**
+     * 保存定位获取的城市(带时间戳)
+     */
+    fun saveLocationCity(city: String) {
+        prefs.edit()
+            .putString(KEY_LOCATION_CITY, city)
+            .putLong(KEY_LOCATION_TIME, System.currentTimeMillis())
+            .apply()
+    }
+
+    /**
+     * 获取缓存的定位城市(如果未过期)
+     */
+    fun getCachedLocationCity(): String? {
+        val city = prefs.getString(KEY_LOCATION_CITY, null)
+        val time = prefs.getLong(KEY_LOCATION_TIME, 0)
+        
+        if (city != null && time > 0) {
+            val now = System.currentTimeMillis()
+            if (now - time < CACHE_DURATION) {
+                return city
+            }
+        }
+        return null
+    }
+
+    /**
+     * 清除缓存
+     */
+    fun clearCache() {
+        prefs.edit().clear().apply()
+    }
+}
+

+ 105 - 0
app/src/main/java/com/narutohuo/xindazhou/community/util/LocationHelper.kt

@@ -0,0 +1,105 @@
+package com.narutohuo.xindazhou.community.util
+
+import android.content.Context
+import android.location.Geocoder
+import android.location.Location
+import androidx.core.content.ContextCompat
+import com.google.android.gms.location.*
+import com.narutohuo.xindazhou.core.log.ILog
+import kotlinx.coroutines.suspendCancellableCoroutine
+import java.util.Locale
+import kotlin.coroutines.resume
+
+/**
+ * 定位工具类
+ * 使用 FusedLocationProviderClient 获取位置,Geocoder 进行逆地理编码
+ */
+class LocationHelper(private val context: Context) {
+
+    private val fusedLocationClient: FusedLocationProviderClient =
+        LocationServices.getFusedLocationProviderClient(context)
+
+    /**
+     * 获取当前位置的城市名称
+     * @return 城市名称,失败返回 null
+     */
+    suspend fun getCurrentCity(): String? {
+        return try {
+            // 检查 Geocoder 是否可用
+            if (!Geocoder.isPresent()) {
+                ILog.w("LocationHelper", "Geocoder 不可用")
+                return null
+            }
+
+            // 获取位置
+            val location = getCurrentLocation() ?: return null
+
+            // 逆地理编码
+            val geocoder = Geocoder(context, Locale.getDefault())
+            val addresses = geocoder.getFromLocation(location.latitude, location.longitude, 1)
+
+            if (addresses?.isNotEmpty() == true) {
+                val address = addresses[0]
+                // 优先使用 locality(城市),如果没有则使用 adminArea(省/直辖市)
+                val city = address.locality ?: address.adminArea
+                ILog.d("LocationHelper", "获取到城市: $city")
+                city
+            } else {
+                ILog.w("LocationHelper", "逆地理编码返回空结果")
+                null
+            }
+        } catch (e: Exception) {
+            ILog.e("LocationHelper", "获取城市失败", e)
+            null
+        }
+    }
+
+    /**
+     * 获取当前位置
+     */
+    private suspend fun getCurrentLocation(): Location? = suspendCancellableCoroutine { continuation ->
+        try {
+            val locationRequest = LocationRequest.Builder(Priority.PRIORITY_BALANCED_POWER_ACCURACY, 10000)
+                .setMaxUpdateDelayMillis(5000)
+                .build()
+
+            val locationCallback = object : LocationCallback() {
+                override fun onLocationResult(locationResult: LocationResult) {
+                    fusedLocationClient.removeLocationUpdates(this)
+                    continuation.resume(locationResult.lastLocation)
+                }
+            }
+
+            fusedLocationClient.requestLocationUpdates(
+                locationRequest,
+                locationCallback,
+                context.mainLooper
+            )
+
+            continuation.invokeOnCancellation {
+                fusedLocationClient.removeLocationUpdates(locationCallback)
+            }
+        } catch (e: SecurityException) {
+            ILog.e("LocationHelper", "定位权限被拒绝", e)
+            continuation.resume(null)
+        } catch (e: Exception) {
+            ILog.e("LocationHelper", "获取位置失败", e)
+            continuation.resume(null)
+        }
+    }
+
+    /**
+     * 检查是否有定位权限
+     */
+    fun hasLocationPermission(): Boolean {
+        return ContextCompat.checkSelfPermission(
+            context,
+            android.Manifest.permission.ACCESS_FINE_LOCATION
+        ) == android.content.pm.PackageManager.PERMISSION_GRANTED ||
+                ContextCompat.checkSelfPermission(
+                    context,
+                    android.Manifest.permission.ACCESS_COARSE_LOCATION
+                ) == android.content.pm.PackageManager.PERMISSION_GRANTED
+    }
+}
+

+ 11 - 16
app/src/main/java/com/narutohuo/xindazhou/launch/AppInitializer.kt

@@ -4,13 +4,12 @@ import android.app.Application
 import com.alibaba.android.arouter.launcher.ARouter
 import com.narutohuo.xindazhou.auth.AuthManager
 import com.narutohuo.xindazhou.common.config.ServerConfigManager
-import com.narutohuo.xindazhou.core.log.ILog
 import com.narutohuo.xindazhou.common.network.ApiManager
 import com.narutohuo.xindazhou.common.version.VersionUpdateManager
-import com.narutohuo.xindazhou.socketio.SocketIOManager
+import com.narutohuo.xindazhou.core.log.ILog
 import com.narutohuo.xindazhou.core.storage.StorageImpl
-import com.narutohuo.xindazhou.share.ShareKit
 import com.narutohuo.xindazhou.push.PushKit
+import com.narutohuo.xindazhou.share.ShareKit
 
 /**
  * 应用初始化管理器
@@ -58,24 +57,20 @@ object AppInitializer {
             // 4. 初始化配置管理器(base-common)
             ServerConfigManager.init(application.applicationContext)
             
-            // 5. 网络管理器(base-common,完全懒加载)
-            // ApiManager 会在第一次 create() 时自动初始化,无需手动调用
-            ILog.d(TAG, "✅ 网络管理器(完全懒加载,首次使用时自动初始化)")
-            
-            // 6. 设置认证管理器上下文(懒加载模式,不立即初始化)
-            // AuthManager 会在第一次使用时自动初始化,并配置 NetworkHelper 的 Token Provider
-            AuthManager.setContext(application.applicationContext)
-            ILog.d(TAG, "✅ 认证管理器上下文已设置(懒加载模式)")
+            // 5. 设置认证管理器上下文(懒加载模式,不立即初始化)
+            // AuthManager 会在第一次使用时自动初始化,并统一配置所有认证相关模块:
+            // - Network 模块的 Token Provider、刷新 Provider 和刷新失败回调
+            // - SocketIO 模块的 Token 刷新 Provider 和登录状态检查 Provider
+            AuthManager.setContext(application)
+            ILog.d(TAG, "✅ 认证管理器上下文已设置(懒加载模式,统一管理所有认证相关配置)")
             
             // 7. 初始化版本更新管理器(base-common)
             VersionUpdateManager.init(application.applicationContext)
             ILog.d(TAG, "✅ 版本更新管理器初始化完成")
             
             // 8. SocketIO 管理器(capability-socketio,完全懒加载)
-            // 设置回调(app 层依赖 base-common,可以直接使用 AuthManager)
-            SocketIOManager.isLoggedInProvider = { AuthManager.isLoggedIn() }
-            SocketIOManager.refreshTokenProvider = { AuthManager.refreshTokenIfNeeded() }
-            ILog.d(TAG, "✅ SocketIO 管理器回调已设置(完全懒加载,首次使用时自动初始化)")
+            // 注意:Token 刷新和登录状态检查的回调已在 AuthManager.setContext() 中统一配置
+            ILog.d(TAG, "✅ SocketIO 管理器(完全懒加载,首次使用时自动初始化,Token 刷新配置已统一管理)")
             
             // 9. 初始化分享服务(capability-share,可选)
             initShareService(application)
@@ -100,7 +95,7 @@ object AppInitializer {
     private fun initLogging(application: Application) {
         val isDebug = isDebugMode(application)
         // 传入 application,内部会自动注册 ActivityLifecycleCallbacks 来启动 LogcatViewer
-        com.narutohuo.xindazhou.core.log.ILog.init(application, enableLogging = isDebug)
+        ILog.init(application, enableLogging = isDebug)
         ILog.d(TAG, "日志系统初始化完成(使用 LogcatViewer),将在 Activity 启动时自动显示悬浮按钮")
     }
     

+ 10 - 9
app/src/main/java/com/narutohuo/xindazhou/service/ui/ServiceFragment.kt

@@ -20,15 +20,16 @@ class ServiceFragment : BaseFragment<FragmentServiceBinding>() {
     }
     
     override fun initView() {
-        // 设置内容
-        val textView = TextView(requireContext())
-        textView.text = "服务页面\n(待实现)"
-        textView.textSize = 18f
-        textView.gravity = android.view.Gravity.CENTER
-        
-        // 将内容添加到布局中
-        binding.contentContainer.removeAllViews()
-        binding.contentContainer.addView(textView)
+        // 使用新的布局文件,设置点击事件
+        binding.ivSettings.setOnClickListener {
+            // TODO: 跳转到设置页面
+            android.widget.Toast.makeText(requireContext(), "设置功能待实现", android.widget.Toast.LENGTH_SHORT).show()
+        }
+    }
+    
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        initView()
     }
 }
 

+ 19 - 15
app/src/main/java/com/narutohuo/xindazhou/user/ui/UserFragment.kt

@@ -38,24 +38,28 @@ class UserFragment : BaseFragment<FragmentUserBinding>() {
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
         
-        // 初始化 ViewModel
-        viewModel = ViewModelProvider(
-            requireActivity(),
-            UserViewModelFactory(requireActivity().application)
-        )[UserViewModel::class.java]
-        
-        // 初始化内容布局
-        contentBinding = FragmentUserSocketTestBinding.bind(binding.contentLayout.root)
-        
-        initViews()
-        setupClickListeners()
-        observeUiState()
-        observeAlarmMessage()
-        observeErrorMessage()
+        // 使用新的个人中心布局,不再使用 SocketIO 测试布局
+        // 如果需要 SocketIO 测试功能,可以单独创建一个测试页面
+        initView()
     }
     
     override fun initView() {
-        // UI初始化在onViewCreated中完成
+        // 设置个人中心界面的点击事件
+        val profileLayout = binding.contentLayout.root
+        
+        // 设置按钮
+        profileLayout.findViewById<android.view.View>(com.narutohuo.xindazhou.R.id.ivSettings)?.setOnClickListener {
+            // TODO: 跳转到设置页面
+            Toast.makeText(requireContext(), "设置功能待实现", Toast.LENGTH_SHORT).show()
+        }
+        
+        // 日历按钮
+        profileLayout.findViewById<android.view.View>(com.narutohuo.xindazhou.R.id.ivCalendar)?.setOnClickListener {
+            // TODO: 跳转到日历页面
+            Toast.makeText(requireContext(), "日历功能待实现", Toast.LENGTH_SHORT).show()
+        }
+        
+        // 其他功能按钮的点击事件可以在这里添加
     }
     
     private fun initViews() {

+ 35 - 0
app/src/main/java/com/narutohuo/xindazhou/vehicle/datasource/remote/VehicleRemoteDataSource.kt

@@ -0,0 +1,35 @@
+package com.narutohuo.xindazhou.vehicle.datasource.remote
+
+import com.narutohuo.xindazhou.common.network.response.ApiResponse
+import com.narutohuo.xindazhou.vehicle.datasource.remote.http.VehicleApi
+import com.narutohuo.xindazhou.vehicle.model.Vehicle
+import com.narutohuo.xindazhou.vehicle.model.VehicleBindRequest
+import com.narutohuo.xindazhou.community.model.PageResult
+
+/**
+ * 车辆远程数据源接口
+ */
+interface VehicleRemoteDataSource {
+    
+    suspend fun bindVehicle(request: VehicleBindRequest): ApiResponse<Vehicle>
+    
+    suspend fun getVehicleList(
+        pageNo: Int = 1,
+        pageSize: Int = 10
+    ): ApiResponse<PageResult<Vehicle>>
+    
+    suspend fun getVehicle(id: Long): ApiResponse<Vehicle>
+    
+    suspend fun updateVehicle(
+        id: Long,
+        name: String? = null,
+        imageUrl: String? = null
+    ): ApiResponse<Boolean>
+    
+    suspend fun setDefaultVehicle(vehicleId: Long): ApiResponse<Boolean>
+    
+    suspend fun unbindVehicle(vehicleId: Long): ApiResponse<Boolean>
+    
+    suspend fun getVehicleStatus(vehicleId: Long): ApiResponse<VehicleApi.VehicleStatus>
+}
+

+ 69 - 0
app/src/main/java/com/narutohuo/xindazhou/vehicle/datasource/remote/VehicleRemoteDataSourceImpl.kt

@@ -0,0 +1,69 @@
+package com.narutohuo.xindazhou.vehicle.datasource.remote
+
+import com.narutohuo.xindazhou.common.network.ApiBaseRemoteDataSource
+import com.narutohuo.xindazhou.common.network.ApiManager
+import com.narutohuo.xindazhou.common.network.response.ApiResponse
+import com.narutohuo.xindazhou.community.model.PageResult
+import com.narutohuo.xindazhou.vehicle.datasource.remote.http.VehicleApi
+import com.narutohuo.xindazhou.vehicle.model.Vehicle
+import com.narutohuo.xindazhou.vehicle.model.VehicleBindRequest
+
+/**
+ * 车辆远程数据源实现
+ */
+class VehicleRemoteDataSourceImpl : ApiBaseRemoteDataSource(), VehicleRemoteDataSource {
+    
+    private val vehicleApi: VehicleApi by lazy {
+        ApiManager.create<VehicleApi>()
+    }
+    
+    override suspend fun bindVehicle(request: VehicleBindRequest): ApiResponse<Vehicle> {
+        return executeRequestResponse(
+            request = { vehicleApi.bindVehicle(request) }
+        )
+    }
+    
+    override suspend fun getVehicleList(
+        pageNo: Int,
+        pageSize: Int
+    ): ApiResponse<PageResult<Vehicle>> {
+        return executeRequestResponse(
+            request = { vehicleApi.getVehicleList(pageNo, pageSize) }
+        )
+    }
+    
+    override suspend fun getVehicle(id: Long): ApiResponse<Vehicle> {
+        return executeRequestResponse(
+            request = { vehicleApi.getVehicle(id) }
+        )
+    }
+    
+    override suspend fun updateVehicle(
+        id: Long,
+        name: String?,
+        imageUrl: String?
+    ): ApiResponse<Boolean> {
+        return executeRequestResponse(
+            request = { vehicleApi.updateVehicle(VehicleApi.UpdateVehicleRequest(id, name, imageUrl)) }
+        )
+    }
+    
+    override suspend fun setDefaultVehicle(vehicleId: Long): ApiResponse<Boolean> {
+        return executeRequestResponse(
+            request = { vehicleApi.setDefaultVehicle(vehicleId) }
+        )
+    }
+    
+    override suspend fun unbindVehicle(vehicleId: Long): ApiResponse<Boolean> {
+        return executeRequestResponse(
+            request = { vehicleApi.unbindVehicle(vehicleId) }
+        )
+    }
+    
+    override suspend fun getVehicleStatus(vehicleId: Long): ApiResponse<VehicleApi.VehicleStatus> {
+        return executeRequestResponse(
+            request = { vehicleApi.getVehicleStatus(vehicleId) }
+        )
+    }
+}
+

+ 97 - 0
app/src/main/java/com/narutohuo/xindazhou/vehicle/datasource/remote/http/VehicleApi.kt

@@ -0,0 +1,97 @@
+package com.narutohuo.xindazhou.vehicle.datasource.remote.http
+
+import com.narutohuo.xindazhou.common.network.ApiCommonResult
+import com.narutohuo.xindazhou.vehicle.model.Vehicle
+import com.narutohuo.xindazhou.vehicle.model.VehicleBindRequest
+import retrofit2.Response
+import retrofit2.http.*
+
+/**
+ * 车辆相关API接口
+ */
+interface VehicleApi {
+    
+    /**
+     * 绑定车辆
+     */
+    @POST("vehicle/bind")
+    suspend fun bindVehicle(
+        @Body request: VehicleBindRequest
+    ): Response<ApiCommonResult<Vehicle>>
+    
+    /**
+     * 获取车辆列表
+     */
+    @GET("vehicle/list")
+    suspend fun getVehicleList(
+        @Query("pageNo") pageNo: Int = 1,
+        @Query("pageSize") pageSize: Int = 10
+    ): Response<ApiCommonResult<com.narutohuo.xindazhou.community.model.PageResult<Vehicle>>>
+    
+    /**
+     * 获取车辆详情
+     */
+    @GET("vehicle/get")
+    suspend fun getVehicle(
+        @Query("id") id: Long
+    ): Response<ApiCommonResult<Vehicle>>
+    
+    /**
+     * 更新车辆信息
+     */
+    @PUT("vehicle/update")
+    suspend fun updateVehicle(
+        @Body request: UpdateVehicleRequest
+    ): Response<ApiCommonResult<Boolean>>
+    
+    /**
+     * 设置默认车辆
+     */
+    @PUT("vehicle/set-default")
+    suspend fun setDefaultVehicle(
+        @Query("vehicleId") vehicleId: Long
+    ): Response<ApiCommonResult<Boolean>>
+    
+    /**
+     * 解绑车辆
+     */
+    @DELETE("vehicle/unbind")
+    suspend fun unbindVehicle(
+        @Query("vehicleId") vehicleId: Long
+    ): Response<ApiCommonResult<Boolean>>
+    
+    /**
+     * 获取车辆状态
+     */
+    @GET("vehicle/status")
+    suspend fun getVehicleStatus(
+        @Query("vehicleId") vehicleId: Long
+    ): Response<ApiCommonResult<VehicleStatus>>
+    
+    /**
+     * 更新车辆请求
+     */
+    data class UpdateVehicleRequest(
+        val id: Long,
+        val name: String? = null,
+        val imageUrl: String? = null
+    )
+    
+    /**
+     * 车辆状态
+     */
+    data class VehicleStatus(
+        val vehicleId: Long,
+        val defenseStatus: Int? = null,
+        val powerStatus: Int? = null,
+        val doorLockStatus: Int? = null,
+        val batteryLevel: Int? = null,
+        val signalStrength: Int? = null,
+        val bluetoothStatus: Int? = null,
+        val range: Int? = null,
+        val frontTirePressure: Double? = null,
+        val rearTirePressure: Double? = null,
+        val updateTime: String? = null
+    )
+}
+

+ 47 - 0
app/src/main/java/com/narutohuo/xindazhou/vehicle/model/Vehicle.kt

@@ -0,0 +1,47 @@
+package com.narutohuo.xindazhou.vehicle.model
+
+/**
+ * 车辆信息
+ */
+data class Vehicle(
+    val id: Long? = null,
+    val vin: String? = null,
+    val deviceSn: String? = null,
+    val name: String? = null,
+    val bluetoothMac: String? = null,
+    val imageUrl: String? = null,
+    val modelId: Long? = null,
+    val firmwareVersion: String? = null,
+    val status: Int? = null,
+    val batteryLevel: Int? = null,
+    val range: Int? = null,
+    val isDefault: Int? = null,
+    val userType: Int? = null,
+    val bindTime: String? = null
+)
+
+/**
+ * 车辆绑定请求
+ */
+data class VehicleBindRequest(
+    val vin: String? = null,
+    val deviceSn: String? = null,
+    val name: String,
+    val bluetoothMac: String? = null,
+    val imageUrl: String? = null,
+    val modelId: Long? = null,
+    val firmwareVersion: String? = null,
+    val userType: Int = 2 // 默认车主
+)
+
+/**
+ * 车辆信息(从蓝牙获取)
+ */
+data class VehicleInfo(
+    val vin: String? = null,
+    val deviceSn: String? = null,
+    val bluetoothMac: String? = null,
+    val modelId: Long? = null,
+    val firmwareVersion: String? = null
+)
+

+ 52 - 0
app/src/main/java/com/narutohuo/xindazhou/vehicle/repository/VehicleRepository.kt

@@ -0,0 +1,52 @@
+package com.narutohuo.xindazhou.vehicle.repository
+
+import com.narutohuo.xindazhou.common.network.response.ApiResponse
+import com.narutohuo.xindazhou.community.model.PageResult
+import com.narutohuo.xindazhou.vehicle.datasource.remote.VehicleRemoteDataSource
+import com.narutohuo.xindazhou.vehicle.datasource.remote.http.VehicleApi
+import com.narutohuo.xindazhou.vehicle.model.Vehicle
+import com.narutohuo.xindazhou.vehicle.model.VehicleBindRequest
+
+/**
+ * 车辆数据仓库
+ */
+class VehicleRepository(
+    private val remoteDataSource: VehicleRemoteDataSource
+) {
+    
+    suspend fun bindVehicle(request: VehicleBindRequest): ApiResponse<Vehicle> {
+        return remoteDataSource.bindVehicle(request)
+    }
+    
+    suspend fun getVehicleList(
+        pageNo: Int = 1,
+        pageSize: Int = 10
+    ): ApiResponse<PageResult<Vehicle>> {
+        return remoteDataSource.getVehicleList(pageNo, pageSize)
+    }
+    
+    suspend fun getVehicle(id: Long): ApiResponse<Vehicle> {
+        return remoteDataSource.getVehicle(id)
+    }
+    
+    suspend fun updateVehicle(
+        id: Long,
+        name: String? = null,
+        imageUrl: String? = null
+    ): ApiResponse<Boolean> {
+        return remoteDataSource.updateVehicle(id, name, imageUrl)
+    }
+    
+    suspend fun setDefaultVehicle(vehicleId: Long): ApiResponse<Boolean> {
+        return remoteDataSource.setDefaultVehicle(vehicleId)
+    }
+    
+    suspend fun unbindVehicle(vehicleId: Long): ApiResponse<Boolean> {
+        return remoteDataSource.unbindVehicle(vehicleId)
+    }
+    
+    suspend fun getVehicleStatus(vehicleId: Long): ApiResponse<VehicleApi.VehicleStatus> {
+        return remoteDataSource.getVehicleStatus(vehicleId)
+    }
+}
+

Plik diff jest za duży
+ 329 - 936
app/src/main/java/com/narutohuo/xindazhou/vehicle/ui/VehicleFragment.kt


+ 368 - 0
app/src/main/java/com/narutohuo/xindazhou/vehicle/ui/binding/VehicleBindActivity.kt

@@ -0,0 +1,368 @@
+package com.narutohuo.xindazhou.vehicle.ui.binding
+
+import android.Manifest
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.Toast
+import androidx.activity.viewModels
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.lifecycleScope
+import com.narutohuo.xindazhou.ble.BLEKit
+import com.narutohuo.xindazhou.ble.api.IBLE
+import com.narutohuo.xindazhou.ble.config.BLEConstants
+import com.narutohuo.xindazhou.common.ui.BaseActivity
+import com.narutohuo.xindazhou.core.log.ILog
+import com.narutohuo.xindazhou.databinding.ActivityVehicleBindBinding
+import com.narutohuo.xindazhou.qrcode.api.QRCodeManager
+import com.narutohuo.xindazhou.qrcode.factory.QRCodeManagerFactory
+import com.narutohuo.xindazhou.qrcode.model.QRCodeResponse
+import com.narutohuo.xindazhou.vehicle.model.VehicleInfo
+import com.narutohuo.xindazhou.vehicle.ui.viewmodel.VehicleBindState
+import com.narutohuo.xindazhou.vehicle.ui.viewmodel.VehicleBindViewModel
+import com.narutohuo.xindazhou.vehicle.ui.viewmodel.VehicleBindViewModelFactory
+import kotlinx.coroutines.launch
+
+/**
+ * 车辆绑定 Activity
+ * 
+ * 流程:
+ * 1. 扫码(扫描车辆SN二维码/条形码)
+ * 2. 蓝牙连接(使用扫码获取的信息)
+ * 3. 获取车辆信息
+ * 4. 输入车辆名称并绑定
+ */
+class VehicleBindActivity : BaseActivity<ActivityVehicleBindBinding>() {
+    
+    private val viewModel: VehicleBindViewModel by viewModels {
+        VehicleBindViewModelFactory(application)
+    }
+    
+    private lateinit var qrCodeManager: QRCodeManager
+    private lateinit var bleService: IBLE
+    
+    // 扫码获取的设备信息
+    private var scannedDeviceSn: String? = null
+    private var scannedBluetoothMac: String? = null
+    
+    // 蓝牙获取的车辆信息
+    private var vehicleInfo: VehicleInfo? = null
+    
+    override fun getViewBinding(): ActivityVehicleBindBinding {
+        return ActivityVehicleBindBinding.inflate(layoutInflater)
+    }
+    
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        
+        // 初始化扫码管理器
+        qrCodeManager = QRCodeManagerFactory.create()
+        
+        // 初始化蓝牙服务
+        bleService = BLEKit.create(this)
+        
+        initView()
+        observeState()
+        
+        // 自动开始扫码(使用扫码模块封装的界面)
+        startScanQRCode()
+    }
+    
+    override fun initView() {
+        // 不需要初始化UI,因为扫码界面由QRCodeScanActivity提供
+    }
+    
+    private fun observeState() {
+        lifecycleScope.launch {
+            viewModel.bindState.collect { state ->
+                when (state) {
+                    is VehicleBindState.Idle -> {
+                        // 初始状态
+                    }
+                    is VehicleBindState.Scanning -> {
+                        // 蓝牙扫描中
+                    }
+                    is VehicleBindState.DeviceFound -> {
+                        // 设备已找到
+                    }
+                    is VehicleBindState.Connecting -> {
+                        // 蓝牙连接中
+                        Toast.makeText(this@VehicleBindActivity, "正在连接设备...", Toast.LENGTH_SHORT).show()
+                    }
+                    is VehicleBindState.Connected -> {
+                        // 连接成功,查询车辆信息
+                        queryVehicleInfo()
+                    }
+                    is VehicleBindState.Binding -> {
+                        // 绑定中
+                        Toast.makeText(this@VehicleBindActivity, "正在绑定车辆...", Toast.LENGTH_SHORT).show()
+                    }
+                    is VehicleBindState.Success -> {
+                        // 绑定成功
+                        Toast.makeText(this@VehicleBindActivity, "绑定成功!", Toast.LENGTH_SHORT).show()
+                        setResult(RESULT_OK)
+                        finish()
+                    }
+                    is VehicleBindState.Error -> {
+                        Toast.makeText(this@VehicleBindActivity, state.message, Toast.LENGTH_SHORT).show()
+                    }
+                }
+            }
+        }
+    }
+    
+    /**
+     * 开始扫码
+     * 
+     * 使用扫码模块封装的界面(QRCodeScanActivity)
+     * 扫码界面已经在capability-qrcode模块中实现,包括:
+     * - 扫码预览区域
+     * - 扫码框和扫描线动画
+     * - 返回按钮
+     * - 提示文字
+     * 业务层只需要调用scanQRCode方法即可
+     */
+    private fun startScanQRCode() {
+        ILog.d("VehicleBindActivity", "启动扫码界面...")
+        
+        // 使用扫码模块封装的界面,会自动处理权限和界面显示
+        qrCodeManager.scanQRCode(this) { response ->
+            runOnUiThread {
+                if (response.success && response.data != null) {
+                    ILog.d("VehicleBindActivity", "扫码成功: ${response.data}")
+                    handleQRCodeScanned(response.data!!)
+                } else {
+                    ILog.e("VehicleBindActivity", "扫码失败: ${response.errorMessage}")
+                    // 如果用户取消扫码,直接返回
+                    if (response.errorMessage == "用户取消扫码") {
+                        finish()
+                    } else {
+                        Toast.makeText(this, "扫码失败: ${response.errorMessage}", Toast.LENGTH_SHORT).show()
+                        // 扫码失败后可以继续扫码
+                        startScanQRCode()
+                    }
+                }
+            }
+        }
+    }
+    
+    /**
+     * 处理扫码结果
+     * 
+     * 扫码内容可能是:
+     * - 设备SN(字符串)
+     * - JSON格式:{"sn": "xxx", "mac": "xx:xx:xx:xx:xx:xx"}
+     * - 手动输入的内容
+     * - "BLUETOOTH_SCAN"(蓝牙扫描标记)
+     * - 其他格式
+     */
+    private fun handleQRCodeScanned(content: String) {
+        ILog.d("VehicleBindActivity", "处理扫码结果: $content")
+        
+        // 检查是否是蓝牙扫描
+        if (content == "BLUETOOTH_SCAN") {
+            // 直接进行蓝牙扫描连接(不使用扫码信息)
+            ILog.d("VehicleBindActivity", "使用蓝牙扫描方式")
+            connectBluetooth()
+            return
+        }
+        
+        // 处理扫码或手动输入的内容
+        try {
+            // 尝试解析JSON格式
+            val jsonObject = org.json.JSONObject(content)
+            scannedDeviceSn = jsonObject.optString("sn", null)
+            scannedBluetoothMac = jsonObject.optString("mac", null)
+        } catch (e: Exception) {
+            // 如果不是JSON,可能是纯SN字符串(扫码或手动输入)
+            scannedDeviceSn = content.trim()
+        }
+        
+        if (scannedDeviceSn.isNullOrBlank()) {
+            Toast.makeText(this, "内容无效,请重新扫码或输入", Toast.LENGTH_SHORT).show()
+            startScanQRCode()
+            return
+        }
+        
+        ILog.d("VehicleBindActivity", "设备SN: $scannedDeviceSn, MAC: $scannedBluetoothMac")
+        
+        // 扫码成功后,开始蓝牙连接
+        connectBluetooth()
+    }
+    
+    /**
+     * 连接蓝牙设备
+     */
+    private fun connectBluetooth() {
+        // 检查蓝牙权限
+        val requiredPermissions = mutableListOf<String>()
+        
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            requiredPermissions.add(Manifest.permission.BLUETOOTH_SCAN)
+            requiredPermissions.add(Manifest.permission.BLUETOOTH_CONNECT)
+        } else {
+            requiredPermissions.add(Manifest.permission.BLUETOOTH)
+            requiredPermissions.add(Manifest.permission.BLUETOOTH_ADMIN)
+            requiredPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION)
+        }
+        
+        val missingPermissions = requiredPermissions.filter { permission ->
+            ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED
+        }
+        
+        if (missingPermissions.isNotEmpty()) {
+            requestPermissions(missingPermissions.toTypedArray(), REQUEST_CODE_BLUETOOTH_PERMISSION)
+            return
+        }
+        
+        // 权限已授予,开始连接
+        performBluetoothConnect()
+    }
+    
+    override fun onRequestPermissionsResult(
+        requestCode: Int,
+        permissions: Array<out String>,
+        grantResults: IntArray
+    ) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+        
+        // 相机权限由QRCodeScanActivity处理,这里只处理蓝牙权限
+        when (requestCode) {
+            REQUEST_CODE_BLUETOOTH_PERMISSION -> {
+                val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
+                if (allGranted) {
+                    performBluetoothConnect()
+                } else {
+                    Toast.makeText(this, "需要蓝牙权限才能连接设备", Toast.LENGTH_LONG).show()
+                }
+            }
+        }
+    }
+    
+    /**
+     * 执行蓝牙连接
+     */
+    private fun performBluetoothConnect() {
+        viewModel.setConnecting()
+        
+        // 获取用户ID(从TokenStore获取,这里简化处理)
+        val userId = ByteArray(16) { it.toByte() } // TODO: 从TokenStore获取真实用户ID
+        val userType = BLEConstants.USER_TYPE_OWNER
+        
+        ILog.d("VehicleBindActivity", "开始蓝牙连接...")
+        
+        // 如果有MAC地址,可以指定设备连接;否则使用自动扫描连接
+        bleService.initializeAndConnect(
+            userId = userId,
+            userType = userType,
+            onConnected = {
+                runOnUiThread {
+                    ILog.d("VehicleBindActivity", "蓝牙连接成功")
+                    viewModel.setVehicleInfo(VehicleInfo(
+                        deviceSn = scannedDeviceSn,
+                        bluetoothMac = scannedBluetoothMac
+                    ))
+                }
+            },
+            onDisconnected = {
+                runOnUiThread {
+                    ILog.d("VehicleBindActivity", "蓝牙连接已断开")
+                    Toast.makeText(this, "连接已断开", Toast.LENGTH_SHORT).show()
+                }
+            },
+            onDataReceived = { data ->
+                ILog.d("VehicleBindActivity", "收到数据: ${data.size} bytes")
+            },
+            onError = { error ->
+                runOnUiThread {
+                    ILog.e("VehicleBindActivity", "蓝牙连接失败: $error")
+                    viewModel.resetState()
+                    Toast.makeText(this, "连接失败: $error", Toast.LENGTH_SHORT).show()
+                }
+            }
+        )
+    }
+    
+    /**
+     * 查询车辆信息
+     */
+    private fun queryVehicleInfo() {
+        if (!bleService.isConnected()) {
+            Toast.makeText(this, "设备未连接", Toast.LENGTH_SHORT).show()
+            return
+        }
+        
+        ILog.d("VehicleBindActivity", "查询车辆信息...")
+        
+        bleService.queryVehicleInfo { response ->
+            runOnUiThread {
+                if (response.success && response.data != null) {
+                    ILog.d("VehicleBindActivity", "车辆信息查询成功")
+                    // 解析车辆信息
+                    val data = response.data
+                    if (data != null) {
+                        parseVehicleInfo(data)
+                    } else {
+                        Toast.makeText(this@VehicleBindActivity, "车辆信息数据为空", Toast.LENGTH_SHORT).show()
+                    }
+                } else {
+                    ILog.e("VehicleBindActivity", "车辆信息查询失败: ${response.errorMessage}")
+                    Toast.makeText(this@VehicleBindActivity, "查询车辆信息失败: ${response.errorMessage}", Toast.LENGTH_SHORT).show()
+                }
+            }
+        }
+    }
+    
+    /**
+     * 解析车辆信息
+     */
+    private fun parseVehicleInfo(data: ByteArray) {
+        if (data.size < 84) {
+            Toast.makeText(this, "车辆信息数据不完整", Toast.LENGTH_SHORT).show()
+            return
+        }
+        
+        // TODO: 根据实际协议解析车辆信息
+        // 这里简化处理,实际需要根据蓝牙协议文档解析
+        val vin = "VIN_${System.currentTimeMillis()}" // 临时值,实际应从data解析
+        
+        vehicleInfo = VehicleInfo(
+            vin = vin,
+            deviceSn = scannedDeviceSn,
+            bluetoothMac = scannedBluetoothMac,
+            modelId = null,
+            firmwareVersion = null
+        )
+        
+        // 更新ViewModel
+        viewModel.setVehicleInfo(vehicleInfo!!)
+        
+        // 跳转到输入车辆名称界面
+        // TODO: 可以显示一个Dialog或跳转到新界面输入车辆名称
+        showVehicleNameInputDialog()
+    }
+    
+    /**
+     * 显示车辆名称输入对话框
+     */
+    private fun showVehicleNameInputDialog() {
+        // TODO: 实现输入对话框
+        // 暂时直接使用默认名称绑定
+        viewModel.bindVehicle("我的车辆")
+    }
+    
+    override fun onDestroy() {
+        super.onDestroy()
+        // 断开蓝牙连接
+        if (::bleService.isInitialized && bleService.isConnected()) {
+            bleService.disconnect()
+        }
+    }
+    
+    companion object {
+        private const val REQUEST_CODE_BLUETOOTH_PERMISSION = 1002
+    }
+}

+ 18 - 0
app/src/main/java/com/narutohuo/xindazhou/vehicle/ui/viewmodel/VehicleBindState.kt

@@ -0,0 +1,18 @@
+package com.narutohuo.xindazhou.vehicle.ui.viewmodel
+
+import com.narutohuo.xindazhou.vehicle.model.VehicleInfo
+
+/**
+ * 车辆绑定状态
+ */
+sealed class VehicleBindState {
+    object Idle : VehicleBindState()
+    object Scanning : VehicleBindState()
+    data class DeviceFound(val deviceName: String, val deviceAddress: String) : VehicleBindState()
+    object Connecting : VehicleBindState()
+    data class Connected(val vehicleInfo: VehicleInfo?) : VehicleBindState()
+    data class Binding(val vehicleName: String) : VehicleBindState()
+    data class Success(val vehicleId: Long) : VehicleBindState()
+    data class Error(val message: String) : VehicleBindState()
+}
+

+ 90 - 0
app/src/main/java/com/narutohuo/xindazhou/vehicle/ui/viewmodel/VehicleBindViewModel.kt

@@ -0,0 +1,90 @@
+package com.narutohuo.xindazhou.vehicle.ui.viewmodel
+
+import android.app.Application
+import androidx.lifecycle.viewModelScope
+import com.narutohuo.xindazhou.common.network.response.ApiResponse
+import com.narutohuo.xindazhou.common.network.response.onError
+import com.narutohuo.xindazhou.common.network.response.onException
+import com.narutohuo.xindazhou.common.network.response.onSuccess
+import com.narutohuo.xindazhou.common.ui.BaseViewModel
+import com.narutohuo.xindazhou.vehicle.model.Vehicle
+import com.narutohuo.xindazhou.vehicle.model.VehicleBindRequest
+import com.narutohuo.xindazhou.vehicle.model.VehicleInfo
+import com.narutohuo.xindazhou.vehicle.repository.VehicleRepository
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+/**
+ * 车辆绑定 ViewModel
+ */
+class VehicleBindViewModel(
+    application: Application,
+    private val repository: VehicleRepository
+) : BaseViewModel(application) {
+    
+    private val _bindState = MutableStateFlow<VehicleBindState>(VehicleBindState.Idle)
+    val bindState: StateFlow<VehicleBindState> = _bindState
+    
+    private var currentVehicleInfo: VehicleInfo? = null
+    
+    /**
+     * 设置车辆信息(从蓝牙获取)
+     */
+    fun setVehicleInfo(vehicleInfo: VehicleInfo) {
+        currentVehicleInfo = vehicleInfo
+        _bindState.value = VehicleBindState.Connected(vehicleInfo)
+    }
+    
+    /**
+     * 绑定车辆
+     */
+    fun bindVehicle(vehicleName: String) {
+        if (currentVehicleInfo == null) {
+            _bindState.value = VehicleBindState.Error("车辆信息为空,请先连接设备")
+            return
+        }
+        
+        _bindState.value = VehicleBindState.Binding(vehicleName)
+        
+        viewModelScope.launch {
+            val request = VehicleBindRequest(
+                vin = currentVehicleInfo?.vin,
+                deviceSn = currentVehicleInfo?.deviceSn,
+                name = vehicleName,
+                bluetoothMac = currentVehicleInfo?.bluetoothMac,
+                imageUrl = null,
+                modelId = currentVehicleInfo?.modelId,
+                firmwareVersion = currentVehicleInfo?.firmwareVersion,
+                userType = 2 // 车主
+            )
+            
+            repository.bindVehicle(request)
+                .onSuccess { vehicle ->
+                    _bindState.value = VehicleBindState.Success(vehicle.id ?: 0L)
+                }
+                .onError { _, message ->
+                    _bindState.value = VehicleBindState.Error(message ?: "绑定失败")
+                }
+                .onException { exception ->
+                    _bindState.value = VehicleBindState.Error(exception.message ?: "网络错误")
+                }
+        }
+    }
+    
+    /**
+     * 设置连接中状态
+     */
+    fun setConnecting() {
+        _bindState.value = VehicleBindState.Connecting
+    }
+    
+    /**
+     * 重置状态
+     */
+    fun resetState() {
+        currentVehicleInfo = null
+        _bindState.value = VehicleBindState.Idle
+    }
+}
+

+ 26 - 0
app/src/main/java/com/narutohuo/xindazhou/vehicle/ui/viewmodel/VehicleBindViewModelFactory.kt

@@ -0,0 +1,26 @@
+package com.narutohuo.xindazhou.vehicle.ui.viewmodel
+
+import android.app.Application
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.narutohuo.xindazhou.vehicle.datasource.remote.VehicleRemoteDataSourceImpl
+import com.narutohuo.xindazhou.vehicle.repository.VehicleRepository
+
+/**
+ * VehicleBindViewModel 工厂类
+ */
+class VehicleBindViewModelFactory(
+    private val application: Application
+) : ViewModelProvider.Factory {
+    
+    @Suppress("UNCHECKED_CAST")
+    override fun <T : ViewModel> create(modelClass: Class<T>): T {
+        val repository = VehicleRepository(VehicleRemoteDataSourceImpl())
+        
+        if (modelClass.isAssignableFrom(VehicleBindViewModel::class.java)) {
+            return VehicleBindViewModel(application, repository) as T
+        }
+        throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
+    }
+}
+

+ 8 - 0
app/src/main/res/color/bottom_nav_item_color_selector.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- 选中状态:科技蓝 -->
+    <item android:color="@color/accent_primary" android:state_checked="true" />
+    <!-- 未选中状态:浅灰色 -->
+    <item android:color="@color/text_secondary" />
+</selector>
+

+ 9 - 0
app/src/main/res/color/bottom_nav_item_color_selector_dark.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- TabBar 深色主题颜色选择器 -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- 选中状态:绿色 -->
+    <item android:color="@color/tabbar_selected_dark" android:state_checked="true" />
+    <!-- 未选中状态:半透明白色 -->
+    <item android:color="@color/tabbar_unselected_dark" />
+</selector>
+

+ 6 - 0
app/src/main/res/drawable/bg_avatar_placeholder.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <solid android:color="@color/bg_secondary" />
+</shape>
+

+ 10 - 0
app/src/main/res/drawable/bg_button_outline.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@android:color/transparent" />
+    <stroke
+        android:width="1dp"
+        android:color="@color/accent_primary" />
+    <corners android:radius="8dp" />
+</shape>
+

+ 7 - 0
app/src/main/res/drawable/bg_button_primary.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/accent_primary" />
+    <corners android:radius="8dp" />
+</shape>
+

+ 19 - 0
app/src/main/res/drawable/bg_button_primary_glass.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 玻璃拟态主要按钮背景 -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- 按下状态 -->
+    <item android:state_pressed="true">
+        <shape android:shape="rectangle">
+            <solid android:color="@color/accent_primary_dark" />
+            <corners android:radius="@dimen/corner_radius_large" />
+        </shape>
+    </item>
+    <!-- 默认状态 -->
+    <item>
+        <shape android:shape="rectangle">
+            <solid android:color="@color/accent_primary" />
+            <corners android:radius="@dimen/corner_radius_large" />
+        </shape>
+    </item>
+</selector>
+

+ 7 - 0
app/src/main/res/drawable/bg_gradient_primary.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 主背景 - 温暖的米白色(不使用渐变) -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/bg_primary" />
+</shape>
+

+ 8 - 0
app/src/main/res/drawable/bg_home_indicator.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Home Indicator (iOS 风格) -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/text_on_dark_primary" />
+    <corners android:radius="100dp" />
+</shape>
+

+ 8 - 0
app/src/main/res/drawable/bg_info_circle.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <solid android:color="@android:color/transparent" />
+    <stroke
+        android:width="2dp"
+        android:color="@color/accent_secondary" />
+</shape>

+ 7 - 0
app/src/main/res/drawable/bg_input.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/bg_secondary" />
+    <corners android:radius="8dp" />
+</shape>
+

+ 11 - 0
app/src/main/res/drawable/bg_location_gradient.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 位置卡片底部渐变遮罩 -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <gradient
+        android:angle="179"
+        android:startColor="#00000000"
+        android:endColor="#B0000000"
+        android:type="linear" />
+</shape>
+

+ 7 - 0
app/src/main/res/drawable/bg_notification_badge.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 通知红点 -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <solid android:color="#EFFB06" />
+</shape>
+

+ 6 - 0
app/src/main/res/drawable/bg_tab_selected.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/bg_secondary" />
+    <corners android:radius="8dp" />
+</shape>

+ 6 - 0
app/src/main/res/drawable/bg_tab_unselected.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <corners android:radius="18dp" />
+    <solid android:color="@color/bg_secondary" />
+</shape>

+ 7 - 0
app/src/main/res/drawable/bg_tabbar_dark.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- TabBar 深色背景(带半透明效果,模糊效果需要在代码中实现) -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/tabbar_bg_dark" />
+</shape>
+

+ 11 - 0
app/src/main/res/drawable/bg_tire_pressure_button.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 胎压按钮背景:半透明白色 rgba(255,255,255,0.1),圆角 32dp,边框 rgba(255,255,255,0.8) -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/bg_vehicle_button_glass" />
+    <corners android:radius="32dp" />
+    <stroke
+        android:width="1dp"
+        android:color="#CCFFFFFF" />
+</shape>
+

+ 12 - 0
app/src/main/res/drawable/bg_vehicle_blur_mask.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 车况页模糊遮罩层 -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <gradient
+        android:angle="179"
+        android:startColor="#000A0A10"
+        android:centerColor="#5C0A0A10"
+        android:endColor="#0A0A10"
+        android:type="linear" />
+</shape>
+

+ 8 - 0
app/src/main/res/drawable/bg_vehicle_button_active.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 车况页激活状态按钮背景 #34c759 -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/control_button_active" />
+    <corners android:radius="@dimen/corner_radius_circle" />
+</shape>
+

+ 11 - 0
app/src/main/res/drawable/bg_vehicle_button_glass.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 车况页半透明白色按钮背景 rgba(255,255,255,0.1) -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/bg_vehicle_button_glass" />
+    <corners android:radius="@dimen/corner_radius_circle" />
+    <stroke
+        android:width="1dp"
+        android:color="#CCFFFFFF" />
+</shape>
+

+ 8 - 0
app/src/main/res/drawable/bg_vehicle_card_glass.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 车况页半透明卡片背景 rgba(120,120,128,0.16) -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/bg_vehicle_card_glass" />
+    <corners android:radius="@dimen/corner_radius_medium" />
+</shape>
+

+ 8 - 0
app/src/main/res/drawable/bg_vehicle_card_glass_10dp.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 车况页半透明卡片背景 rgba(120,120,128,0.16),圆角 10dp(用于服务与帮助和查看更多车型) -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/bg_vehicle_card_glass" />
+    <corners android:radius="10dp" />
+</shape>
+

+ 12 - 0
app/src/main/res/drawable/bg_vehicle_gradient.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 车况页背景渐变 -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <gradient
+        android:angle="179"
+        android:startColor="#141419"
+        android:centerColor="#8D8D9B"
+        android:endColor="#9494A3"
+        android:type="linear" />
+</shape>
+

BIN
app/src/main/res/drawable/common_bluetooth.png


+ 13 - 0
app/src/main/res/drawable/common_signal.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 信号图标 -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android" 
+    android:width="24dp" 
+    android:height="24dp" 
+    android:viewportWidth="24" 
+    android:viewportHeight="24">
+    <!-- 信号强度指示器(3条竖线) -->
+    <path 
+        android:fillColor="#FFFFFF" 
+        android:pathData="M2 18h2v-6H2v6zM6 18h2v-10H6v10zM10 18h2v-14h-2v14z" />
+</vector>
+

BIN
app/src/main/res/drawable/ic_add.png


+ 11 - 0
app/src/main/res/drawable/ic_arrow_right.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 右箭头图标 -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="10dp"
+    android:height="10dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="#8C8C8C"
+        android:pathData="M8.59,16.59L13.17,12 8.59,7.41 10,6l6,6 -6,6z" />
+</vector>

+ 11 - 0
app/src/main/res/drawable/ic_comment_outline.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M21,6h-2v9H6v2c0,0.55 0.45,1 1,1h11l4,4V7c0,-0.55 -0.45,-1 -1,-1zM17,12V3c0,-0.55 -0.45,-1 -1,-1H3C2.45,2 2,2.45 2,3v14l4,-4h10c0.55,0 1,-0.45 1,-1z"/>
+</vector>
+

BIN
app/src/main/res/drawable/ic_decor_90.png


+ 11 - 0
app/src/main/res/drawable/ic_default_avatar.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
+</vector>
+

BIN
app/src/main/res/drawable/ic_driving_record.png


BIN
app/src/main/res/drawable/ic_key_share.png


+ 11 - 0
app/src/main/res/drawable/ic_like_outline.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z"/>
+</vector>
+

BIN
app/src/main/res/drawable/ic_location_decor.png


BIN
app/src/main/res/drawable/ic_location_pin.png


BIN
app/src/main/res/drawable/ic_map_mask.png


BIN
app/src/main/res/drawable/ic_navigation.png


BIN
app/src/main/res/drawable/ic_notification.png


BIN
app/src/main/res/drawable/ic_odo.png


BIN
app/src/main/res/drawable/ic_power.png


+ 7 - 0
app/src/main/res/drawable/ic_search.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
+    <path android:fillColor="#FFFFFF" android:pathData="M24 0H0V24H24V0Z" />
+    <path android:fillColor="#FFFFFF" android:pathData="M22.8416 22.6942L17.3309 17.05C18.6309 15.55 19.75 13.1 19.75 10.95C19.75 6.15 15.8 2.20001 11 2.20001C6.19999 2.20001 2.25 6.2 2.25 11C2.25 15.8 6.19999 19.7159 11 19.7159C12.067 19.7376 12.8129 19.6707 13.2377 19.5153" android:strokeColor="#FFFFFF" android:strokeWidth="1.5" />
+    <path android:fillColor="#FFFFFF" android:pathData="M13 6C14.5343 6.51086 15.6795 7.80139 16.0043 9.38551L16.1189 9.94403" android:strokeColor="#EFFB06" android:strokeWidth="1.5" />
+    <path android:fillColor="#FFFFFF" android:pathData="M13 6C14.5343 6.51086 15.6795 7.80139 16.0043 9.38551L16.1189 9.94403" android:strokeColor="#FFFFFF" android:strokeWidth="1.5" />
+</vector>

BIN
app/src/main/res/drawable/ic_search_decor.png


+ 11 - 0
app/src/main/res/drawable/ic_share.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
+</vector>
+

BIN
app/src/main/res/drawable/ic_skip.png


BIN
app/src/main/res/drawable/ic_smart_accessories.png


BIN
app/src/main/res/drawable/ic_smart_health.png


+ 11 - 0
app/src/main/res/drawable/ic_star_outline.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
+</vector>
+

BIN
app/src/main/res/drawable/ic_store_decor.png


BIN
app/src/main/res/drawable/ic_tire_normal.png


BIN
app/src/main/res/drawable/ic_tire_warning.png


BIN
app/src/main/res/drawable/map_placeholder.png


BIN
app/src/main/res/drawable/progress_bar_bg.png


BIN
app/src/main/res/drawable/service_online_support.png


BIN
app/src/main/res/drawable/service_repair.png


BIN
app/src/main/res/drawable/service_smart.png


+ 0 - 0
app/src/main/res/drawable/service_store_offline.png


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików