From 8b61e88c0a58d5fe6e59b56a2f238f2fda87399c Mon Sep 17 00:00:00 2001
From: Hsy <32729842@qq.com>
Date: Sat, 14 Feb 2026 11:04:01 +0800
Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
composeApp/build.gradle.kts | 27 +-
.../src/androidMain/AndroidManifest.xml | 19 +-
.../kotlin/com/taskttl/MainActivity.kt | 6 +-
.../taskttl/core/utils/DeviceUtils.android.kt | 7 +-
.../taskttl/core/utils/ToastUtils.android.kt | 7 +
.../domain/repository/AuthRepositoryImpl.kt | 6 +-
.../drawable/ic_empty_data.xml | 21 ++
.../drawable/ic_empty_error.xml | 12 +
.../drawable/ic_empty_network.xml | 15 +
.../composeResources/drawable/ic_error.xml | 26 ++
.../composeResources/values/strings.xml | 7 +
.../commonMain/kotlin/com/taskttl/app/App.kt | 3 +-
.../kotlin/com/taskttl/app/di/KoinModels.kt | 48 ++-
.../com/taskttl/core/auth/AuthManager.kt | 128 +++++++
.../kotlin/com/taskttl/core/base/BaseState.kt | 8 +-
.../com/taskttl/core/base/BaseViewModel.kt | 10 +
.../core/config/FlattenBaseReqSerializer.kt | 4 +
.../core/designsystem/component/Spacer.kt | 141 ++++++++
.../taskttl/core/designsystem/theme/Color.kt | 306 ++++++++++++++++
.../taskttl/core/designsystem/theme/Shape.kt | 83 +++++
.../taskttl/core/designsystem/theme/Size.kt | 117 ++++++
.../taskttl/core/designsystem/theme/Theme.kt | 106 ++++++
.../taskttl/core/designsystem/theme/Type.kt | 195 ++++++++++
.../kotlin/com/taskttl/core/domain/BaseReq.kt | 2 +
.../com/taskttl/core/network/ApiConfig.kt | 9 +-
.../taskttl/core/network/AuthHttpClient.kt | 28 ++
.../com/taskttl/core/network/KtorClient.kt | 69 +++-
.../core/ui/component/divider/Divider.kt | 19 +
.../taskttl/core/ui/component/empty/Empty.kt | 121 +++++++
.../core/ui/component/empty/EmptyData.kt | 36 ++
.../core/ui/component/empty/EmptyError.kt | 49 +++
.../core/ui/component/empty/EmptyNetwork.kt | 48 +++
.../com/taskttl/core/utils/StorageUtils.kt | 51 ++-
.../com/taskttl/data/constant/Constant.kt | 7 +
.../repository/CountdownRepositoryImpl.kt | 11 +
.../data/repository/TaskRepositoryImpl.kt | 14 +
.../data/source/local/dao/CategoryDao.kt | 10 +-
.../data/source/local/dao/CountdownDao.kt | 7 +
.../taskttl/data/source/local/dao/TaskDao.kt | 6 +
.../data/source/remote/api/TaskTTLApi.kt | 41 ++-
.../source/remote/dto/request/LoginReq.kt | 24 --
.../remote/dto/request/RefreshTokenRequest.kt | 16 +
.../remote/dto/request/ThirdPartyLoginReq.kt | 39 ++
.../source/remote/dto/response/AuthResult.kt | 6 +
.../dto/response/RefreshTokenResponse.kt | 32 ++
.../dto/response/ThirdPartyLoginResp.kt | 55 +++
.../com/taskttl/domain/model/UserInfo.kt | 21 ++
.../domain/repository/CountdownRepository.kt | 10 +
.../domain/repository/TaskRepository.kt | 10 +
.../kotlin/com/taskttl/navigation/MainNav.kt | 48 ++-
.../kotlin/com/taskttl/navigation/Routes.kt | 32 +-
.../common/components/ActionButtonListItem.kt | 4 +-
.../common/components/NetworkImage.kt | 9 +
.../common/foundation/GlobalImageLoader.kt | 3 +
.../presentation/common/theme/Theme.kt | 139 -------
.../taskttl/presentation/common/theme/Type.kt | 40 ---
.../features/auth/AuthViewModel.kt | 90 -----
.../features/category/list/CategoryScreen.kt | 26 +-
.../features/category/list/CategoryState.kt | 17 +-
.../countdown/list/CountdownScreen.kt | 22 +-
.../features/countdown/list/CountdownState.kt | 16 +-
.../features/onboarding/OnboardingScreen.kt | 2 +-
.../features/onboarding/OnboardingState.kt | 2 +-
.../onboarding/OnboardingViewModel.kt | 2 +-
.../features/settings/about/AboutScreen.kt | 11 +-
.../dataManagement/DataManagementScreen.kt | 339 +++++-------------
.../dataManagement/DataManagementState.kt | 61 ++++
.../dataManagement/DataManagementViewModel.kt | 62 ++++
.../components/ConfirmDialog.kt | 53 +++
.../components/DataManagementCard.kt | 92 +++++
.../components/ExportDataDialog.kt | 81 +++++
.../components/ImportDataDialog.kt | 68 ++++
.../settings/feedback/FeedbackScreen.kt | 7 +-
.../settings/feedback/FeedbackState.kt | 11 +-
.../features/settings/main/SettingsScreen.kt | 40 ++-
.../features/settings/main/SettingsState.kt | 5 +-
.../settings/main/SettingsViewModel.kt | 10 +-
.../settings/main/common/UserInfoCard.kt | 35 +-
.../settings/privacy/PrivacyScreen.kt | 6 +-
.../features/splash/SplashScreen.kt | 11 +-
.../features/splash/SplashState.kt | 2 +-
.../features/splash/SplashViewModel.kt | 10 +-
.../features/statistics/StatisticsScreen.kt | 165 +--------
.../components/CategoryStatisticItem.kt | 134 +++++++
.../statistics/components/StatisticCard.kt | 76 ++++
.../features/task/list/TaskScreen.kt | 23 +-
.../features/task/list/TaskState.kt | 13 +-
.../{auth => user/login}/LoginScreen.kt | 177 ++++-----
.../AuthState.kt => user/login/LoginState.kt} | 34 +-
.../features/user/login/LoginViewModel.kt | 104 ++++++
.../user/userProfile/UserProfileScreen.kt | 241 +++++++++++++
.../user/userProfile/UserProfileState.kt | 55 +++
.../user/userProfile/UserProfileViewModel.kt | 37 ++
.../user/userProfile/components/AvatarView.kt | 66 ++++
.../user/userProfile/components/EmailRow.kt | 41 +++
.../user/userProfile/components/InfoRow.kt | 57 +++
.../userProfile/components/NicknameEditor.kt | 55 +++
.../kotlin/com/taskttl/MainViewController.kt | 1 +
gradle/libs.versions.toml | 57 ++-
99 files changed, 3689 insertions(+), 1046 deletions(-)
create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_empty_data.xml
create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_empty_error.xml
create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_empty_network.xml
create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_error.xml
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/auth/AuthManager.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/component/Spacer.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Color.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Shape.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Size.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Theme.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Type.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/network/AuthHttpClient.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/divider/Divider.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/Empty.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyData.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyError.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyNetwork.kt
delete mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/LoginReq.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/RefreshTokenRequest.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/ThirdPartyLoginReq.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/RefreshTokenResponse.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/ThirdPartyLoginResp.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/domain/model/UserInfo.kt
delete mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/theme/Theme.kt
delete mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/theme/Type.kt
delete mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/AuthViewModel.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementState.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementViewModel.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ConfirmDialog.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/DataManagementCard.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ExportDataDialog.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ImportDataDialog.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/components/CategoryStatisticItem.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/components/StatisticCard.kt
rename composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/{auth => user/login}/LoginScreen.kt (53%)
rename composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/{auth/AuthState.kt => user/login/LoginState.kt} (57%)
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginViewModel.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileScreen.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileState.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileViewModel.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/AvatarView.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/EmailRow.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/InfoRow.kt
create mode 100644 composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/NicknameEditor.kt
diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index 1ced809..862ad92 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -49,7 +49,7 @@ kotlin {
sourceSets {
androidMain.dependencies {
- implementation(compose.preview)
+ implementation(libs.jetbrains.ui.tooling.preview)
implementation(libs.androidx.activity.compose)
// 启动
@@ -70,8 +70,8 @@ kotlin {
// sqlite
implementation(libs.androidx.room.sqlite.wrapper)
- // admob
- implementation(libs.android.play.services.ads.identifier)
+ // ads identifier
+ implementation(libs.androidx.ads.identifier)
// work
implementation(libs.androidx.work)
@@ -84,12 +84,12 @@ kotlin {
implementation(libs.androidx.login.facebook)
}
commonMain.dependencies {
- implementation(compose.runtime)
- implementation(compose.foundation)
- implementation(compose.material3)
- implementation(compose.ui)
- implementation(compose.components.resources)
- implementation(compose.components.uiToolingPreview)
+ implementation(libs.runtime)
+ implementation(libs.foundation)
+ implementation(libs.material3)
+ implementation(libs.ui)
+ implementation(libs.components.resources)
+ implementation(libs.jetbrains.ui.tooling.preview)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.savedstate)
implementation(libs.androidx.lifecycle.runtimeCompose)
@@ -97,11 +97,14 @@ kotlin {
// 导航
implementation(libs.navigation.compose)
+ // implementation(libs.androidx.navigation3.ui)
+ // implementation(libs.androidx.navigation3.material3.adaptive)
// Koin依赖注入
implementation(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.viewmodel)
+ implementation(libs.koin.navigation3)
// Ktor网络请求
implementation(libs.ktor.client.core)
@@ -113,7 +116,7 @@ kotlin {
implementation(libs.coil3.compose)
implementation(libs.coil3.svg)
// implementation(libs.coil3.gif)
- // implementation(libs.coil3.network.ktor3)
+ implementation(libs.coil3.network.ktor3)
// 添加日期时间处理依赖
implementation(libs.kotlinx.datetime)
@@ -162,7 +165,7 @@ android {
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = libs.versions.android.versionCode.get().toInt()
- versionName = libs.versions.android.versionName.get().toString()
+ versionName = libs.versions.android.versionName.get()
buildConfigField("String", "APP_NAME", "\"TaskTTL\"")
@@ -206,7 +209,7 @@ android {
}
dependencies {
- debugImplementation(compose.uiTooling)
+ debugImplementation(libs.ui.tooling)
// add("kspCommonMainMetadata",libs.androidx.room.compiler)
// add("kspCommonMain",libs.androidx.room.compiler)
// add("kspWasmJs",libs.androidx.room.compiler)
diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml
index 264168c..4020f07 100644
--- a/composeApp/src/androidMain/AndroidManifest.xml
+++ b/composeApp/src/androidMain/AndroidManifest.xml
@@ -14,19 +14,25 @@
+
+
+
+
@@ -41,10 +47,9 @@
-
+
+
+
@@ -70,7 +75,7 @@
android:name=".core.receiver.AlarmReceiver"
android:enabled="true"
android:exported="true"
- >
+ android:permission="com.taskttl.permission.ALARM">
diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/taskttl/MainActivity.kt
index 61d1fa8..5166e52 100644
--- a/composeApp/src/androidMain/kotlin/com/taskttl/MainActivity.kt
+++ b/composeApp/src/androidMain/kotlin/com/taskttl/MainActivity.kt
@@ -14,9 +14,9 @@ import com.taskttl.app.App
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// ✅ Android 12+ 启动系统原生 Splash
- val splashScreen = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- installSplashScreen()
- } else null
+ // val splashScreen = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ // installSplashScreen()
+ // } else null
// splashScreen?.apply {
// // setKeepOnScreenCondition { isChecking }
// }
diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/DeviceUtils.android.kt b/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/DeviceUtils.android.kt
index 13a26b6..2772d9e 100644
--- a/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/DeviceUtils.android.kt
+++ b/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/DeviceUtils.android.kt
@@ -69,6 +69,7 @@ actual object DeviceUtils {
uniqueId = getUniqueId(),
deviceInfo = Build.MODEL ?: "0",
deviceVersion = Build.VERSION.RELEASE,
+ country = getSimOrNetworkCountry(),
language = Localization.getDeviceLanguage()
)
}
@@ -97,6 +98,7 @@ actual object DeviceUtils {
*/
private fun isValidAdId(adId: String?): Boolean {
if (adId.isNullOrBlank()) return false
+ // Regex("^0{8}-0{4}-0{4}-0{4}-0{12}$")
if (adId == "00000000-0000-0000-0000-000000000000") return false
if (adId.matches(Regex("^0+$"))) return false
return true
@@ -119,9 +121,10 @@ actual object DeviceUtils {
fun getSimOrNetworkCountry(): String {
val tm = appContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
+
return when {
- !tm.networkCountryIso.isNullOrEmpty() -> tm.networkCountryIso.uppercase()
- !tm.simCountryIso.isNullOrEmpty() -> tm.simCountryIso.uppercase()
+ !tm.networkCountryIso.isNullOrBlank() -> tm.networkCountryIso.uppercase()
+ !tm.simCountryIso.isNullOrBlank() -> tm.simCountryIso.uppercase()
else -> ""
}
}
diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/ToastUtils.android.kt b/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/ToastUtils.android.kt
index afbaedd..76fed67 100644
--- a/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/ToastUtils.android.kt
+++ b/composeApp/src/androidMain/kotlin/com/taskttl/core/utils/ToastUtils.android.kt
@@ -7,4 +7,11 @@ actual object ToastUtils {
actual fun show(message: String) {
Toast.makeText(MainApplication.instance.applicationContext, message, Toast.LENGTH_SHORT).show()
}
+
+ fun showWarning(message: String) {
+ // val params = ToastParams()
+ // params.text = text
+ // params.style = CustomToastStyle(R.layout.toast_warn)
+ // Toaster.show(params)
+ }
}
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/com/taskttl/domain/repository/AuthRepositoryImpl.kt b/composeApp/src/androidMain/kotlin/com/taskttl/domain/repository/AuthRepositoryImpl.kt
index 8983421..d4f7bfc 100644
--- a/composeApp/src/androidMain/kotlin/com/taskttl/domain/repository/AuthRepositoryImpl.kt
+++ b/composeApp/src/androidMain/kotlin/com/taskttl/domain/repository/AuthRepositoryImpl.kt
@@ -66,11 +66,9 @@ class AuthRepositoryImpl() : AuthRepository {
AuthResult.Canceled
}
} catch (e: GetCredentialException) {
- e.message?.let { LogUtils.e("DevTTL", it) }
- AuthResult.Error("获取凭据失败: ${e.message}")
+ AuthResult.Error("${e.message}")
} catch (e: Exception) {
- e.message?.let { LogUtils.e("DevTTL", it) }
- AuthResult.Error("Google 登录失败: ${e.message}")
+ AuthResult.Error(" ${e.message}")
}
}
diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_empty_data.xml b/composeApp/src/commonMain/composeResources/drawable/ic_empty_data.xml
new file mode 100644
index 0000000..b684010
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/ic_empty_data.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_empty_error.xml b/composeApp/src/commonMain/composeResources/drawable/ic_empty_error.xml
new file mode 100644
index 0000000..b0c731d
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/ic_empty_error.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_empty_network.xml b/composeApp/src/commonMain/composeResources/drawable/ic_empty_network.xml
new file mode 100644
index 0000000..0fe4c49
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/ic_empty_network.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_error.xml b/composeApp/src/commonMain/composeResources/drawable/ic_error.xml
new file mode 100644
index 0000000..ae4a653
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/ic_error.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index 3b39e6e..207003d 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -316,4 +316,11 @@
EEE, MMM d, yyyy h:mm a
+
+ 暂无数据
+ 加载失败
+ 页面加载出现了问题
+ 网络连接失败
+ 请检查网络连接后重试
+ 重新加载
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/app/App.kt b/composeApp/src/commonMain/kotlin/com/taskttl/app/App.kt
index 5039f33..2a06b3c 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/app/App.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/app/App.kt
@@ -4,10 +4,11 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+import com.taskttl.core.designsystem.theme.AppTheme
import com.taskttl.core.manager.ThemeMode
import com.taskttl.navigation.AppNav
import com.taskttl.presentation.common.foundation.GlobalImageLoader
-import com.taskttl.ui.theme.AppTheme
+
import org.koin.compose.viewmodel.koinViewModel
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/app/di/KoinModels.kt b/composeApp/src/commonMain/kotlin/com/taskttl/app/di/KoinModels.kt
index da52387..ccbe14c 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/app/di/KoinModels.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/app/di/KoinModels.kt
@@ -6,15 +6,18 @@ import com.taskttl.data.repository.OnboardingRepositoryImpl
import com.taskttl.data.repository.SettingsRepositoryImpl
import com.taskttl.domain.repository.OnboardingRepository
import com.taskttl.domain.repository.SettingsRepository
-import com.taskttl.presentation.features.auth.AuthViewModel
import com.taskttl.presentation.features.category.list.CategoryViewModel
import com.taskttl.presentation.features.countdown.list.CountdownViewModel
import com.taskttl.presentation.features.onboarding.OnboardingViewModel
+import com.taskttl.presentation.features.settings.dataManagement.DataManagementViewModel
import com.taskttl.presentation.features.settings.feedback.FeedbackViewModel
import com.taskttl.presentation.features.settings.main.SettingsViewModel
import com.taskttl.presentation.features.splash.SplashViewModel
import com.taskttl.presentation.features.task.list.TaskViewModel
+import com.taskttl.presentation.features.user.login.LoginViewModel
+import com.taskttl.presentation.features.user.userProfile.UserProfileViewModel
import org.koin.core.KoinApplication
+import org.koin.core.annotation.KoinExperimentalAPI
import org.koin.core.context.startKoin
import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModelOf
@@ -44,7 +47,6 @@ val repositoryModule = module {
/** 视图模型模块 */
val viewModelModule = module {
-
viewModelOf(::AppViewModel)
viewModelOf(::SplashViewModel)
viewModelOf(::OnboardingViewModel)
@@ -53,5 +55,45 @@ val viewModelModule = module {
viewModelOf(::CountdownViewModel)
viewModelOf(::SettingsViewModel)
viewModelOf(::FeedbackViewModel)
- viewModelOf(::AuthViewModel)
+ viewModelOf(::LoginViewModel)
+ viewModelOf(::DataManagementViewModel)
+ viewModelOf(::UserProfileViewModel)
}
+
+
+@OptIn(KoinExperimentalAPI::class)
+val appModule = module {
+ // navigation {
+ //
+ //
+ // MainNav()
+ // // SplashScreen(navigatorToRoute = {
+ // // globalNavController.navigate(it) {
+ // // popUpTo { inclusive = true }
+ // // // 确保MainScreen是单例的
+ // // launchSingleTop = true
+ // // }
+ // // })
+ // }
+
+
+ // composable {
+ // SplashScreen(navigatorToRoute = {
+ // globalNavController.navigate(it) {
+ // popUpTo { inclusive = true }
+ // // 确保MainScreen是单例的
+ // launchSingleTop = true
+ // }
+ // })
+ // }
+ // composable {
+ // OnboardingScreen(navigatorToRoute = {
+ // globalNavController.navigate(it) {
+ // popUpTo { inclusive = true }
+ // // 确保MainScreen是单例的
+ // launchSingleTop = true
+ // }
+ // })
+ // }
+ // composable { MainNav() }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/auth/AuthManager.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/auth/AuthManager.kt
new file mode 100644
index 0000000..9a48358
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/auth/AuthManager.kt
@@ -0,0 +1,128 @@
+package com.taskttl.core.auth
+
+import com.taskttl.core.utils.StorageUtils
+import com.taskttl.data.constant.Constant
+import com.taskttl.data.source.remote.api.TaskTTLApi
+import com.taskttl.data.source.remote.dto.request.RefreshTokenRequest
+import com.taskttl.domain.model.UserInfo
+import io.ktor.client.HttpClient
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.serialization.Serializable
+import kotlin.time.Clock
+import kotlin.time.ExperimentalTime
+
+/**
+ * 令牌信息
+ * @author admin
+ * @date 2025/11/23
+ * @constructor 创建[TokenInfo]
+ * @param [accessToken] 访问令牌
+ * @param [refreshToken] 刷新令牌
+ * @param [expiresAt] 到期日
+ */
+@Serializable
+data class TokenInfo(
+ val accessToken: String, // 访问令牌
+ val refreshToken: String, // 刷新令牌
+ val expiresAt: Long, // 过期时间,毫秒时间戳
+)
+
+/**
+ * 负责管理 Token、判断是否过期、刷新 Token
+ */
+object AuthManager {
+
+ private val _tokenInfo =
+ MutableStateFlow(StorageUtils.getObject(Constant.KEY_TOKEN_INFO))
+
+ private val _userInfo =
+ MutableStateFlow(StorageUtils.getObject(Constant.KEY_USER_INFO))
+
+ val userInfo: StateFlow = _userInfo
+
+ private val mutex = Mutex()
+
+ // 预留一个刷新提前量(比如提前 1 分钟刷新)
+ private const val EXPIRE_EARLY_MS = 60_000L
+
+ /**
+ * 更新令牌
+ * @param [info] 信息
+ */
+ fun updateToken(info: TokenInfo?) {
+ _tokenInfo.value = info
+ if (info == null) {
+ StorageUtils.remove(Constant.KEY_TOKEN_INFO)
+ } else {
+ StorageUtils.saveObject(Constant.KEY_TOKEN_INFO, info)
+ }
+ }
+
+ fun updateUserInfo(info: UserInfo?) {
+ _userInfo.value = info
+ if (info == null) {
+ StorageUtils.remove(Constant.KEY_USER_INFO)
+ } else {
+ StorageUtils.saveObject(Constant.KEY_USER_INFO, info)
+ }
+ }
+
+ fun clear() {
+ updateToken(null)
+ updateUserInfo(null)
+ }
+
+ fun currentAccessToken(): String? = _tokenInfo.value?.accessToken
+
+ /**
+ * 已过期
+ * @return [Boolean]
+ */
+ @OptIn(ExperimentalTime::class)
+ fun isExpired(): Boolean {
+ val info = _tokenInfo.value ?: return true
+ val now = Clock.System.now().toEpochMilliseconds()
+ return now >= (info.expiresAt - EXPIRE_EARLY_MS)
+ }
+
+ /**
+ * 对外暴露:获取一个“确保可用”的 AccessToken
+ * - 如果没过期:直接返回
+ * - 如果过期:刷新后返回
+ */
+ suspend fun getValidAccessToken(): String? {
+ return mutex.withLock {
+ // double check,避免并发重复刷新
+ if (!isExpired()) {
+ return _tokenInfo.value?.accessToken
+ }
+ // 已过期,尝试刷新
+ if (tryRefreshToken()) {
+ return _tokenInfo.value?.accessToken
+ }
+ null
+ }
+ }
+
+ /**
+ * 尝试刷新 Token,成功返回 true
+ */
+ @OptIn(ExperimentalTime::class)
+ suspend fun tryRefreshToken(): Boolean {
+ val info = _tokenInfo.value ?: return false
+ if (info.refreshToken.isBlank()) return false
+
+ return try {
+ val refreshToken = RefreshTokenRequest(refreshToken = info.refreshToken)
+ val result = TaskTTLApi.refreshToken(refreshToken)
+ updateToken(result.toTokenInfo())
+ true
+ } catch (_: Exception) {
+ // 刷新失败可以视情况 clear 或保留老的
+ false
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/base/BaseState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/base/BaseState.kt
index 6d1541a..6893af6 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/core/base/BaseState.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/base/BaseState.kt
@@ -13,4 +13,10 @@ open class BaseState(
open val isLoading: Boolean = false,
open val isProcessing: Boolean = false,
open val error: String? = null,
-)
\ No newline at end of file
+)
+
+data class ProcessingState(
+ override val isLoading: Boolean = true, // 是否正在加载
+ override val isProcessing: Boolean = true, // 是否正在处理
+ override val error: String? = null // 错误信息
+) : BaseState(isLoading, isProcessing, error)
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/base/BaseViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/base/BaseViewModel.kt
index a512711..886c459 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/core/base/BaseViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/base/BaseViewModel.kt
@@ -2,6 +2,8 @@ package com.taskttl.core.base
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.taskttl.core.utils.DeviceUtils
+import com.taskttl.core.utils.LogUtils
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
@@ -79,5 +81,13 @@ abstract class BaseViewModel(initialState: S) : ViewModel()
// protected fun clearError() {
// _state.value = _state.value.copy(error = null)
// }
+
+ /**
+ * 日志调试
+ * @param [message] 消息
+ */
+ protected fun logDebug(message: String) {
+ LogUtils.d("DevTTL_" + this::class.simpleName.toString(), message)
+ }
}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/config/FlattenBaseReqSerializer.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/config/FlattenBaseReqSerializer.kt
index b19a536..d9f6dec 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/core/config/FlattenBaseReqSerializer.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/config/FlattenBaseReqSerializer.kt
@@ -27,6 +27,7 @@ class FlattenBaseReqSerializer(
element("uniqueId", PrimitiveSerialDescriptor("uniqueId", PrimitiveKind.STRING))
element("deviceInfo", PrimitiveSerialDescriptor("deviceInfo", PrimitiveKind.STRING))
element("deviceVersion", PrimitiveSerialDescriptor("deviceVersion", PrimitiveKind.STRING))
+ element("country",PrimitiveSerialDescriptor("country", PrimitiveKind.STRING))
element("language", PrimitiveSerialDescriptor("language", PrimitiveKind.STRING))
}
@@ -42,6 +43,7 @@ class FlattenBaseReqSerializer(
jsonObj["uniqueId"] = JsonPrimitive(base.uniqueId)
jsonObj["deviceInfo"] = JsonPrimitive(base.deviceInfo)
jsonObj["deviceVersion"] = JsonPrimitive(base.deviceVersion)
+ jsonObj["country"] = JsonPrimitive(base.country)
jsonObj["language"] = JsonPrimitive(base.language)
jsonEncoder.encodeJsonElement(JsonObject(jsonObj))
@@ -59,6 +61,7 @@ class FlattenBaseReqSerializer(
uniqueId = jsonObj["uniqueId"]!!.jsonPrimitive.content,
deviceInfo = jsonObj["deviceInfo"]!!.jsonPrimitive.content,
deviceVersion = jsonObj["deviceVersion"]!!.jsonPrimitive.content,
+ country = jsonObj["country"]!!.jsonPrimitive.content,
language = jsonObj["language"]!!.jsonPrimitive.content
)
@@ -69,6 +72,7 @@ class FlattenBaseReqSerializer(
jsonObj.remove("uniqueId")
jsonObj.remove("deviceInfo")
jsonObj.remove("deviceVersion")
+ jsonObj.remove("country")
jsonObj.remove("language")
// 反序列化剩下的业务字段
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/component/Spacer.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/component/Spacer.kt
new file mode 100644
index 0000000..8d8b7a8
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/component/Spacer.kt
@@ -0,0 +1,141 @@
+package com.taskttl.core.designsystem.component
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.taskttl.core.designsystem.theme.SpaceHorizontalMedium
+import com.taskttl.core.designsystem.theme.SpaceHorizontalSmall
+import com.taskttl.core.designsystem.theme.SpaceHorizontalXLarge
+import com.taskttl.core.designsystem.theme.SpaceHorizontalXSmall
+import com.taskttl.core.designsystem.theme.SpaceHorizontalXXLarge
+import com.taskttl.core.designsystem.theme.SpaceVerticalLarge
+import com.taskttl.core.designsystem.theme.SpaceVerticalMedium
+import com.taskttl.core.designsystem.theme.SpaceVerticalSmall
+import com.taskttl.core.designsystem.theme.SpaceVerticalXLarge
+import com.taskttl.core.designsystem.theme.SpaceVerticalXSmall
+import com.taskttl.core.designsystem.theme.SpaceVerticalXXLarge
+
+
+/**
+ * 创建一个超大垂直间距(32dp)的Spacer组件
+ * 使用方式:SpaceVerticalXXLarge()
+
+ */
+@Composable
+fun SpaceVerticalXXLarge() {
+ Spacer(modifier = Modifier.height(SpaceVerticalXXLarge))
+}
+
+/**
+ * 创建一个特大垂直间距(24dp)的Spacer组件
+ * 使用方式:SpaceVerticalXLarge()
+
+ */
+@Composable
+fun SpaceVerticalXLarge() {
+ Spacer(modifier = Modifier.height(SpaceVerticalXLarge))
+}
+
+/**
+ * 创建一个大垂直间距(16dp)的Spacer组件
+ * 使用方式:SpaceVerticalLarge()
+
+ */
+@Composable
+fun SpaceVerticalLarge() {
+ Spacer(modifier = Modifier.height(SpaceVerticalLarge))
+}
+
+/**
+ * 创建一个中等垂直间距(12dp)的Spacer组件
+ * 使用方式:SpaceVerticalMedium()
+
+ */
+@Composable
+fun SpaceVerticalMedium() {
+ Spacer(modifier = Modifier.height(SpaceVerticalMedium))
+}
+
+/**
+ * 创建一个小垂直间距(8dp)的Spacer组件
+ * 使用方式:SpaceVerticalSmall()
+
+ */
+@Composable
+fun SpaceVerticalSmall() {
+ Spacer(modifier = Modifier.height(SpaceVerticalSmall))
+}
+
+/**
+ * 创建一个超小垂直间距(4dp)的Spacer组件
+ * 使用方式:SpaceVerticalXSmall()
+
+ */
+@Composable
+fun SpaceVerticalXSmall() {
+ Spacer(modifier = Modifier.height(SpaceVerticalXSmall))
+}
+// endregion
+
+// region 水平间距组件
+/**
+ * 创建一个超大水平间距(32dp)的Spacer组件
+ * 使用方式:SpaceHorizontalXXLarge()
+
+ */
+@Composable
+fun SpaceHorizontalXXLarge() {
+ Spacer(modifier = Modifier.width(SpaceHorizontalXXLarge))
+}
+
+/**
+ * 创建一个特大水平间距(24dp)的Spacer组件
+ * 使用方式:SpaceHorizontalXLarge()
+
+ */
+@Composable
+fun SpaceHorizontalXLarge() {
+ Spacer(modifier = Modifier.width(SpaceHorizontalXLarge))
+}
+
+/**
+ * 创建一个大水平间距(16dp)的Spacer组件
+ * 使用方式:SpaceHorizontalLarge()
+
+ */
+@Composable
+fun SpaceHorizontalLarge() {
+ Spacer(modifier = Modifier.width(SpaceHorizontalXLarge))
+}
+
+/**
+ * 创建一个中等水平间距(12dp)的Spacer组件
+ * 使用方式:SpaceHorizontalMedium()
+
+ */
+@Composable
+fun SpaceHorizontalMedium() {
+ Spacer(modifier = Modifier.width(SpaceHorizontalMedium))
+}
+
+/**
+ * 创建一个小水平间距(8dp)的Spacer组件
+ * 使用方式:SpaceHorizontalSmall()
+
+ */
+@Composable
+fun SpaceHorizontalSmall() {
+ Spacer(modifier = Modifier.width(SpaceHorizontalSmall))
+}
+
+/**
+ * 创建一个超小水平间距(4dp)的Spacer组件
+ * 使用方式:SpaceHorizontalXSmall()
+
+ */
+@Composable
+fun SpaceHorizontalXSmall() {
+ Spacer(modifier = Modifier.width(SpaceHorizontalXSmall))
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Color.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Color.kt
new file mode 100644
index 0000000..8b52e4f
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Color.kt
@@ -0,0 +1,306 @@
+package com.taskttl.core.designsystem.theme
+
+import androidx.compose.ui.graphics.Color
+
+
+/**
+ * 颜色规范
+ * 定义应用程序中使用的所有颜色,包括浅色和深色主题
+ */
+
+/**
+ * 品牌主色(普通模式):#465CFF
+ * 场景:菜单栏、主要按钮、突出文字
+ */
+val PrimaryLight = Color(0xFF465CFF)
+
+/**
+ * 品牌主色(暗黑模式):#466CFF
+ * 场景:暗黑主题下的菜单栏、主要按钮、突出文字
+ */
+val PrimaryDark = Color(0xFF466CFF)
+
+/**
+ * 品牌主色
+ * 暂时和亮色模式一致可自行修改
+ */
+val Primary = PrimaryLight
+
+// 辅助色
+/**
+ * 危险色/红色:#FF2B2B
+ * 适用场景:错误提示、删除操作、警告信息等
+ */
+val ColorDanger = Color(0xFFFF2B2B) // 危险色/红色
+
+/**
+ * 深色模式危险色/红色:#FF2B3B
+ */
+val ColorDangerDark = Color(0xFFFF2B3B)
+
+/**
+ * 警告色/黄色:#FFB703
+ * 适用场景:警告提示、需要注意的信息等
+ */
+val ColorWarning = Color(0xFFFFB703) // 警告色/黄色
+
+/**
+ * 深色模式警告色/黄色:#FFB704
+ */
+val ColorWarningDark = Color(0xFFFFB704)
+
+/**
+ * 紫色:#6831FF
+ * 适用场景:特殊强调、次要品牌色等
+ */
+val ColorPurple = Color(0xFF6831FF) // 紫色
+
+/**
+ * 深色模式紫色:#6832FF
+ */
+val ColorPurpleDark = Color(0xFF6832FF)
+
+/**
+ * 成功色/绿色:#09BE4F
+ * 适用场景:成功提示、完成状态等
+ */
+val ColorSuccess = Color(0xFF09BE4F) // 成功色/绿色
+
+/**
+ * 深色模式成功色/绿色:#09BE5F
+ */
+val ColorSuccessDark = Color(0xFF09BE5F)
+
+// 字体颜色 - 浅色模式
+/**
+ * 浅色模式下主要文字颜色:#181818
+ * 适用场景:标题、重要文本内容
+ */
+val TextPrimaryLight = Color(0xFF181818) // 用于重要标题内容
+
+/**
+ * 浅色模式下次要文字颜色:#333333
+ * 适用场景:正文内容、次要标题
+ */
+val TextSecondaryLight = Color(0xFF333333) // 用于普通内容
+
+/**
+ * 浅色模式下次要标题颜色:#7F7F7F
+ * 适用场景:表单标题、提示标签等
+ */
+val TextSubtitleLight = Color(0xFF7F7F7F)
+
+/**
+ * 浅色模式下三级文字颜色:#B2B2B2
+ * 适用场景:辅助说明、标签文字
+ */
+val TextTertiaryLight = Color(0xFFB2B2B2) // 用于底部标签描述
+
+/**
+ * 浅色模式下四级文字颜色:#CCCCCC
+ * 适用场景:次要辅助信息、禁用状态文字
+ */
+val TextQuaternaryLight = Color(0xFFCCCCCC) // 用于辅助次要信息
+
+/**
+ * 按钮文字:#FFFFFF(两种模式一致)
+ */
+val TextWhite = Color(0xFFFFFFFF)
+
+// 字体颜色 - 深色模式
+/**
+ * 深色模式下主要文字颜色:#D1D1D1
+ * 适用场景:深色模式下的标题、重要文本内容
+ */
+val TextPrimaryDark = Color(0xFFD1D1D1) // 深色模式下的主要文字
+
+/**
+ * 深色模式下次要文字颜色:#A3A3A3
+ * 适用场景:深色模式下的正文内容、次要标题
+ */
+val TextSecondaryDark = Color(0xFFA3A3A3) // 深色模式下的次要文字
+
+/**
+ * 深色模式下次要标题颜色:#8C8C8C
+ * 适用场景:深色模式下的表单标题、提示标签等
+ */
+val TextSubtitleDark = Color(0xFF8C8C8C)
+
+/**
+ * 深色模式下三级文字颜色:#8D8D8D
+ * 适用场景:深色模式下的辅助说明、标签文字
+ */
+val TextTertiaryDark = Color(0xFF8D8D8D) // 深色模式下的三级文字
+
+/**
+ * 深色模式下四级文字颜色:#5E5E5E
+ * 适用场景:深色模式下的次要辅助信息、禁用状态文字
+ */
+val TextQuaternaryDark = Color(0xFF5E5E5E) // 深色模式下的四级文字
+
+// 背景色 - 浅色模式
+/**
+ * 浅色模式下页面背景色:#F1F4FA
+ * 适用场景:应用整体背景、页面底色
+ */
+val BgGreyLight = Color(0xFFF1F4FA) // 页面背景底色
+
+/**
+ * 浅色模式下白色背景:#FFFFFF
+ * 适用场景:卡片、弹窗等内容区域背景
+ */
+val BgWhiteLight = Color(0xFFFFFFFF) // 白色背景
+
+/**
+ * 浅色模式下内容模块背景色:#F8F8F8
+ * 适用场景:次级内容区域、列表项底色
+ */
+val BgContentLight = Color(0xFFF8F8F8) // 内容模块底色
+
+/**
+ * 浅色模式下红色背景:#FF2B2B(5%透明度)
+ * 适用场景:红色主题的轻量化背景、提示区域
+ */
+val BgRedLight = Color(0x0DFF2B2B) // 红色背景 5% 透明度
+
+/**
+ * 浅色模式下黄色背景:#FFB703(10%透明度)
+ * 适用场景:黄色主题的轻量化背景、警告区域
+ */
+val BgYellowLight = Color(0x1AFFB703) // 黄色背景 10% 透明度
+
+/**
+ * 浅色模式下紫色背景:#6831FF(10%透明度)
+ * 适用场景:紫色主题的轻量化背景、特殊区域
+ */
+val BgPurpleLight = Color(0x1A6831FF) // 紫色背景 10% 透明度
+
+/**
+ * 浅色模式下绿色背景:#09BE4F(5%透明度)
+ * 适用场景:绿色主题的轻量化背景、成功提示区域
+ */
+val BgGreenLight = Color(0x0D09BE4F) // 绿色背景 5% 透明度
+
+// 背景色 - 深色模式
+/**
+ * 深色模式下页面背景色:#111111
+ * 适用场景:深色模式下的应用整体背景、页面底色
+ */
+val BgGreyDark = Color(0xFF111111) // 深色模式下的页面背景底色
+
+/**
+ * 深色模式下白色背景:#1B1B1B
+ * 适用场景:深色模式下的卡片、弹窗等内容区域背景
+ */
+val BgWhiteDark = Color(0xFF1B1B1B) // 深色模式下的白色背景
+
+/**
+ * 深色模式下内容模块背景色:#222222
+ * 适用场景:深色模式下的次级内容区域、列表项底色
+ */
+val BgContentDark = Color(0xFF222222) // 深色模式下的内容模块底色
+
+/**
+ * 深色模式下红色背景:#222222
+ */
+val BgRedDark = Color(0xFF222222)
+
+/**
+ * 深色模式下黄色背景:#222222
+ */
+val BgYellowDark = Color(0xFF222222)
+
+/**
+ * 深色模式下紫色背景:#222222
+ */
+val BgPurpleDark = Color(0xFF222222)
+
+/**
+ * 深色模式下绿色背景:#222222
+ */
+val BgGreenDark = Color(0xFF222222)
+
+// 遮罩颜色
+/**
+ * 浅色模式下遮罩颜色:60%透明度黑色
+ * 适用场景:弹窗背景、加载状态遮罩
+ */
+val MaskLight = Color(0x99000000) // rgba(0, 0, 0, 0.6) - 浅色模式
+
+/**
+ * 深色模式下遮罩颜色:60%透明度黑色
+ * 适用场景:深色模式下的弹窗背景、加载状态遮罩
+ */
+val MaskDark = Color(0x99000000) // rgba(0, 0, 0, 0.6) - 深色模式
+
+/**
+ * 浅色模式下按压状态颜色:20%透明度黑色
+ * 适用场景:浅色模式下的按钮、卡片等组件的点击反馈
+ */
+val PressLight = Color(0x33000000) // rgba(0, 0, 0, 0.2) - 浅色模式点击
+
+/**
+ * 深色模式下按压状态颜色:20%透明度白色
+ * 适用场景:深色模式下的按钮、卡片等组件的点击反馈
+ */
+val PressDark = Color(0x33FFFFFF) // rgba(255, 255, 255, .2) - 深色模式点击
+
+/**
+ * 浅色模式下轻量按压状态颜色:5%透明度黑色
+ * 适用场景:弱态组件、背景较浅的点击反馈
+ */
+val PressLightSoft = Color(0x0D000000) // rgba(0, 0, 0, 0.05)
+
+/**
+ * 深色模式下轻量按压状态颜色:10%透明度白色
+ * 适用场景:弱态组件、背景较暗的点击反馈
+ */
+val PressDarkSoft = Color(0x1AFFFFFF) // rgba(255, 255, 255, 0.1)
+
+/**
+ * 浅色模式下阴影颜色:#020426(5%透明度)
+ */
+val ShadowLight = Color(0x0D020426)
+
+/**
+ * 深色模式下阴影颜色:#111111(50%透明度)
+ */
+val ShadowDark = Color(0x80111111)
+
+// 边框颜色
+/**
+ * 浅色模式下边框颜色:#EEEEEE
+ * 适用场景:分割线、边框、描边等
+ */
+val BorderLight = Color(0xFFEEEEEE) // 浅色模式边框
+
+/**
+ * 深色模式下边框颜色:#242424
+ * 适用场景:深色模式下的分割线、边框、描边等
+ */
+val BorderDark = Color(0xFF242424) // 深色模式边框
+
+// 渐变色起点和终点颜色
+/**
+ * 主色渐变起点:#465CFF
+ * 适用场景:与主色渐变终点配合使用,用于渐变按钮、背景等
+ */
+val GradientPrimaryStart = Color(0xFF465CFF) // 主色渐变起点
+
+/**
+ * 主色渐变终点:#6831FF
+ * 适用场景:与主色渐变起点配合使用,用于渐变按钮、背景等
+ */
+val GradientPrimaryEnd = Color(0xFF6831FF) // 主色渐变终点
+
+/**
+ * 红色渐变起点:#FD8C8C
+ * 适用场景:与红色渐变终点配合使用,用于警告类渐变效果
+ */
+val GradientRedStart = Color(0xFFFD8C8C) // 红色渐变起点
+
+/**
+ * 红色渐变终点:#FF2B2B
+ * 适用场景:与红色渐变起点配合使用,用于警告类渐变效果
+ */
+val GradientRedEnd = Color(0xFFFF2B2B) // 红色渐变终点
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Shape.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Shape.kt
new file mode 100644
index 0000000..3e85860
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Shape.kt
@@ -0,0 +1,83 @@
+package com.taskttl.core.designsystem.theme
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Shapes
+import androidx.compose.ui.unit.dp
+
+/**
+ * 圆角规范
+ */
+
+// 标准圆角数值定义
+
+/**
+ * 超小圆角数值:4dp
+ */
+val RadiusXSmall = 4.dp
+
+/**
+ * 小圆角数值:8dp
+ */
+val RadiusSmall = 8.dp
+
+/**
+ * 中圆角数值:12dp
+ */
+val RadiusMedium = 12.dp
+
+/**
+ * 大圆角数值:16dp
+ */
+val RadiusLarge = 16.dp
+
+/**
+ * 超大圆角数值:24dp
+ */
+val RadiusExtraLarge = 24.dp
+
+/**
+ * 超小圆角:4dp
+ * 适用场景:极小的UI元素,如图标按钮
+ */
+val ShapeXSmall = RoundedCornerShape(RadiusXSmall) // 超小圆角 8px
+
+/**
+ * 小圆角:8dp
+ * 适用场景:常规卡片、按钮等小型UI元素的圆角
+ */
+val ShapeSmall = RoundedCornerShape(RadiusSmall) // 小圆角 16px
+
+/**
+ * 中圆角:12dp
+ * 适用场景:中型容器、对话框等组件的圆角
+ */
+val ShapeMedium = RoundedCornerShape(RadiusMedium) // 中圆角 24px
+
+/**
+ * 大圆角:16dp
+ * 适用场景:分类列表等组件
+ */
+val ShapeLarge = RoundedCornerShape(RadiusLarge) // 大圆角 32px
+
+/**
+ * 超大圆角:24dp
+ * 适用场景:大型卡片、底部弹窗等较大UI元素的圆角
+ */
+val ShapeExtraLarge = RoundedCornerShape(RadiusExtraLarge) // 超大圆角 48px
+
+/**
+ * 圆形
+ * 适用场景:头像、图标按钮等需要完全圆形的组件
+ */
+val ShapeCircle = RoundedCornerShape(percent = 50)
+
+/**
+ * Material3 Shapes配置
+ * 将自定义圆角规范应用于Material3设计系统
+ */
+val AppShapes = Shapes(
+ small = ShapeSmall,
+ medium = ShapeMedium,
+ large = ShapeExtraLarge,
+ extraLarge = ShapeExtraLarge
+)
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Size.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Size.kt
new file mode 100644
index 0000000..1f29175
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Size.kt
@@ -0,0 +1,117 @@
+package com.taskttl.core.designsystem.theme
+
+import androidx.compose.ui.unit.dp
+
+/**
+ * 布局间距规范
+ */
+
+/**
+ * 超大垂直间距:32dp
+ * 适用场景:页面块与块之间的大间距,如不同功能模块之间的分隔
+ */
+val SpaceVerticalXXLarge = 32.dp
+
+/**
+ * 特大垂直间距:24dp
+ * 适用场景:大模块内部的分隔,如卡片组之间的间距
+ */
+val SpaceVerticalXLarge = 24.dp
+
+/**
+ * 大垂直间距:16dp
+ * 适用场景:卡片内部主要内容块之间的间距
+ */
+val SpaceVerticalLarge = 16.dp
+
+/**
+ * 中等垂直间距:12dp
+ * 适用场景:内容区域的常规间距,如列表项之间的间距
+ */
+val SpaceVerticalMedium = 12.dp
+
+/**
+ * 小垂直间距:8dp
+ * 适用场景:紧凑布局中的间距,如图标与文字之间
+ */
+val SpaceVerticalSmall = 8.dp
+
+/**
+ * 超小垂直间距:4dp
+ * 适用场景:最小的垂直间隔,如紧凑排列的元素间距
+ */
+val SpaceVerticalXSmall = 4.dp
+
+/**
+ * 超大水平间距:32dp
+ * 适用场景:页面左右边距,大型容器的内边距
+ */
+val SpaceHorizontalXXLarge = 32.dp
+
+/**
+ * 特大水平间距:24dp
+ * 适用场景:大型容器内部的水平分隔
+ */
+val SpaceHorizontalXLarge = 24.dp
+
+/**
+ * 大水平间距:16dp
+ * 适用场景:常规容器的左右边距,如卡片的内边距
+ */
+val SpaceHorizontalLarge = 16.dp
+
+/**
+ * 中等水平间距:12dp
+ * 适用场景:中等容器的水平间距,如列表项的左右间距
+ */
+val SpaceHorizontalMedium = 12.dp
+
+/**
+ * 小水平间距:8dp
+ * 适用场景:紧凑布局的水平间距,如图标与文字之间
+ */
+val SpaceHorizontalSmall = 8.dp
+
+/**
+ * 超小平间间距:4dp
+ * 适用场景:最小的平间间隔,如紧凑排列的元素间距
+ */
+val SpaceHorizontalXSmall = 4.dp
+
+/**
+ * 常用于内边距的值
+ * 大内边距:16dp
+ * 适用场景:卡片、对话框等容器的内边距
+ */
+val SpacePaddingLarge = 16.dp
+
+/**
+ * 中等内边距:12dp
+ * 适用场景:按钮、输入框等中型组件的内边距
+ */
+val SpacePaddingMedium = 12.dp
+
+/**
+ * 小内边距:8dp
+ * 适用场景:紧凑型组件的内边距,如小型按钮
+ */
+val SpacePaddingSmall = 8.dp
+
+/**
+ * 超小内边距:4dp
+ * 适用场景:最小的内边距,如标签、徽章等小组件
+ */
+val SpacePaddingXSmall = 4.dp
+
+/**
+ * 其他常用尺寸
+ * 分割线高度:0.5dp
+ * 适用场景:列表项之间的分割线、边框线等
+ */
+val SpaceDivider = 0.5.dp // 分割线高度
+
+/**
+ * 指示器高度:2dp
+ * 适用场景:选中指示器、进度条等
+ */
+val SpaceIndicator = 2.dp // 指示器高度
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Theme.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Theme.kt
new file mode 100644
index 0000000..b5cd981
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Theme.kt
@@ -0,0 +1,106 @@
+package com.taskttl.core.designsystem.theme
+// 应用主题
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+
+/** 浅色方案 - 聚焦效率与清晰度的 TaskTTL 品牌配色 */
+private val LightColorScheme = lightColorScheme(
+ primary = Color(0xFF3E63F5),
+ onPrimary = Color(0xFFFFFFFF),
+ primaryContainer = Color(0xFFDEE1FF),
+ onPrimaryContainer = Color(0xFF001465),
+ secondary = Color(0xFF0F8B8D),
+ onSecondary = Color(0xFFFFFFFF),
+ secondaryContainer = Color(0xFF99F0EE),
+ onSecondaryContainer = Color(0xFF00201F),
+ tertiary = Color(0xFFFFB155),
+ onTertiary = Color(0xFF432000),
+ tertiaryContainer = Color(0xFFFFDDB4),
+ onTertiaryContainer = Color(0xFF2B1200),
+ error = Color(0xFFB3261E),
+ onError = Color(0xFFFFFFFF),
+ errorContainer = Color(0xFFF9DEDC),
+ onErrorContainer = Color(0xFF410E0B),
+ background = Color(0xFFFAFCFF),
+ onBackground = Color(0xFF111428),
+ surface = Color(0xFFFFFFFF),
+ onSurface = Color(0xFF111428),
+ surfaceVariant = Color(0xFFE0E2EC),
+ onSurfaceVariant = Color(0xFF434654),
+ outline = Color(0xFF747680),
+ outlineVariant = Color(0xFFC4C6D2),
+ scrim = Color(0xFF000000),
+ inverseSurface = Color(0xFF2F3145),
+ inverseOnSurface = Color(0xFFF1F2F8),
+ inversePrimary = Color(0xFFB7C5FF),
+ surfaceDim = Color(0xFFD9DBE4),
+ surfaceBright = Color(0xFFF8F9FF),
+ surfaceContainerLowest = Color(0xFFFDFCFF),
+ surfaceContainerLow = Color(0xFFF3F4FA),
+ surfaceContainer = Color(0xFFEDF0FA),
+ surfaceContainerHigh = Color(0xFFE6E9F5),
+ surfaceContainerHighest = Color(0xFFE0E3F1)
+)
+
+/** 深色方案 - OLED 友好且保持品牌温度 */
+private val DarkColorScheme = darkColorScheme(
+ primary = Color(0xFFB7C5FF),
+ onPrimary = Color(0xFF002178),
+ primaryContainer = Color(0xFF1D3277),
+ onPrimaryContainer = Color(0xFFDEE2FF),
+ secondary = Color(0xFF56D8D1),
+ onSecondary = Color(0xFF003734),
+ secondaryContainer = Color(0xFF00504D),
+ onSecondaryContainer = Color(0xFF99F0EE),
+ tertiary = Color(0xFFFFBA6C),
+ onTertiary = Color(0xFF452803),
+ tertiaryContainer = Color(0xFF653E0F),
+ onTertiaryContainer = Color(0xFFFFDDB4),
+ error = Color(0xFFFFB4AB),
+ onError = Color(0xFF690005),
+ errorContainer = Color(0xFF93000A),
+ onErrorContainer = Color(0xFFFFDAD6),
+ background = Color(0xFF101323),
+ onBackground = Color(0xFFE2E3F1),
+ surface = Color(0xFF101323),
+ onSurface = Color(0xFFE2E3F1),
+ surfaceVariant = Color(0xFF434654),
+ onSurfaceVariant = Color(0xFFC4C6D2),
+ outline = Color(0xFF8E909C),
+ outlineVariant = Color(0xFF434654),
+ scrim = Color(0xFF000000),
+ inverseSurface = Color(0xFFE2E3F1),
+ inverseOnSurface = Color(0xFF26293A),
+ inversePrimary = Color(0xFF3E63F5),
+ surfaceDim = Color(0xFF101323),
+ surfaceBright = Color(0xFF363952),
+ surfaceContainerLowest = Color(0xFF090C19),
+ surfaceContainerLow = Color(0xFF181B2C),
+ surfaceContainer = Color(0xFF1C2032),
+ surfaceContainerHigh = Color(0xFF262A3D),
+ surfaceContainerHighest = Color(0xFF31354A)
+)
+
+/**
+ * 应用主题
+ * @param darkTheme 是否深色模式
+ * @param content 页面内容
+ */
+@Composable
+fun AppTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit,
+) {
+ val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ shapes = AppShapes,
+ content = content
+ )
+}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Type.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Type.kt
new file mode 100644
index 0000000..65c9833
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/designsystem/theme/Type.kt
@@ -0,0 +1,195 @@
+package com.taskttl.core.designsystem.theme
+// 字体规范
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+
+/**
+ * 字体规范
+ * 1px = 0.5sp 转换结果,未提供的尺寸按层级递减补齐
+ */
+val Typography = Typography(
+
+ /**
+ * 中粗体 · 22sp / 31sp
+ * 使用场景:超大标题,文章标题
+ */
+ displayLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.SemiBold, // 中粗体
+ fontSize = 22.sp,
+ lineHeight = 31.sp,
+ letterSpacing = 0.sp
+ ),
+
+ /**
+ * 中粗体 · 18sp / 27sp
+ * 使用场景:大标题
+ */
+ displayMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.SemiBold, // 中粗体
+ fontSize = 18.sp,
+ lineHeight = 27.sp,
+ letterSpacing = 0.sp
+ ),
+
+ /**
+ * 中粗体 · 16sp / 24sp
+ * 使用场景:展示级文案(中等长度)
+ */
+ displaySmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.sp
+ ),
+
+ /**
+ * 中黑体 · 16sp / 24sp
+ * 使用场景:二级标题、导航栏、列表、段落标题、按钮文字
+ */
+ headlineLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium, // 中黑体
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.sp
+ ),
+
+ /**
+ * 中黑体 · 13sp / 20sp
+ * 使用场景:信息分组小标题
+ */
+ headlineSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 13.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.sp
+ ),
+
+ /**
+ * 中黑体 · 14sp / 22sp
+ * 使用场景:类别名称
+ */
+ headlineMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium, // 中黑体
+ fontSize = 14.sp,
+ lineHeight = 22.sp,
+ letterSpacing = 0.sp
+ ),
+
+ /**
+ * 中黑体 · 16sp / 20sp
+ * 使用场景:模块标题、弹窗标题
+ */
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 16.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.sp
+ ),
+
+ /**
+ * 中黑体 · 14sp / 20sp
+ * 使用场景:列表项标题、辅助性标题
+ */
+ titleMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.sp
+ ),
+
+ /**
+ * 中黑体 · 12sp / 18sp
+ * 使用场景:段落内小标题、二级描述
+ */
+ titleSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 12.sp,
+ lineHeight = 18.sp,
+ letterSpacing = 0.sp
+ ),
+
+ /**
+ * 常规体 · 14sp / 22sp
+ * 使用场景:正文内容、段落文字
+ */
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal, // 常规体
+ fontSize = 14.sp,
+ lineHeight = 22.sp,
+ letterSpacing = 0.sp
+ ),
+
+ /**
+ * 常规体 · 12sp / 18sp
+ * 使用场景:底部导航栏文字、辅助性文字、标签文字
+ */
+ bodyMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal, // 常规体
+ fontSize = 12.sp,
+ lineHeight = 18.sp,
+ letterSpacing = 0.sp
+ ),
+
+ /**
+ * 常规体 · 11sp / 16sp
+ * 使用场景:次级正文、辅助段落
+ */
+ bodySmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.sp
+ ),
+
+ /**
+ * 中黑体 · 12sp / 16sp
+ * 使用场景:按钮、标签等操作文字
+ */
+ labelLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.1.sp
+ ),
+
+ /**
+ * 中黑体 · 11sp / 16sp
+ * 使用场景:辅助标签、徽标说明
+ */
+ labelMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.1.sp
+ ),
+
+ /**
+ * 中黑体 · 10sp / 14sp
+ * 使用场景:最小标签、角标
+ */
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 10.sp,
+ lineHeight = 14.sp,
+ letterSpacing = 0.1.sp
+ )
+)
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/BaseReq.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/BaseReq.kt
index 903ffed..f9c2245 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/BaseReq.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/domain/BaseReq.kt
@@ -29,6 +29,7 @@ open class BaseReq(
open val uniqueId: String,
open val deviceInfo: String,
open val deviceVersion: String,
+ open val country: String,
open val language: String,
)
@@ -54,5 +55,6 @@ fun defaultBaseReq(): BaseReq = BaseReq(
uniqueId = "",
deviceInfo = "",
deviceVersion = "",
+ country = "",
language = ""
)
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/network/ApiConfig.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/network/ApiConfig.kt
index dc4582a..27921de 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/core/network/ApiConfig.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/network/ApiConfig.kt
@@ -7,8 +7,8 @@ package com.taskttl.core.network
*/
object ApiConfig {
/** 基本地址 */
- // const val BASE_URL = "http://10.0.0.5:8888/api/v1"
- const val BASE_URL = "https://api.taskttl.com/api/v1"
+ const val BASE_URL = "https://test.svnet.cn/api/v1"
+ // const val BASE_URL = "https://api.taskttl.com/api/v1"
/** 登录地址 */
const val LOGIN_URL = "$BASE_URL/login"
@@ -16,9 +16,12 @@ object ApiConfig {
/** 三方登录地址 */
const val THIRD_PARTY_LOGIN_URL = "$BASE_URL/thirdPartyLogin"
+ /** 刷新令牌url */
+ const val REFRESH_TOKEN_URL = "$BASE_URL/auth/refreshToken"
+
/** 反馈地址 */
const val FEEDBACK_URL = "$BASE_URL/feedback"
/** 埋点地址 */
const val POINT_URL = "$BASE_URL/point"
-}
\ No newline at end of file
+}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/network/AuthHttpClient.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/network/AuthHttpClient.kt
new file mode 100644
index 0000000..ff3b677
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/network/AuthHttpClient.kt
@@ -0,0 +1,28 @@
+package com.taskttl.core.network
+
+import io.ktor.client.HttpClient
+import io.ktor.client.plugins.HttpTimeout
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.serialization.kotlinx.json.json
+import kotlinx.serialization.json.Json
+
+object AuthHttpClient {
+ val client = HttpClient {
+ install(ContentNegotiation) {
+ json(Json {
+ prettyPrint = true
+ isLenient = true
+ ignoreUnknownKeys = true
+ encodeDefaults = false
+ explicitNulls = false
+ })
+ }
+
+ // 可以保留超时、日志等,但**不要**装 HttpSend 里那套 getValidAccessToken/401 刷新逻辑
+ install(HttpTimeout) {
+ connectTimeoutMillis = 15000
+ requestTimeoutMillis = 30000
+ socketTimeoutMillis = 60000
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/network/KtorClient.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/network/KtorClient.kt
index ed9c290..f7b3d29 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/core/network/KtorClient.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/network/KtorClient.kt
@@ -1,18 +1,25 @@
package com.taskttl.core.network
+import com.taskttl.core.auth.AuthManager
import com.taskttl.core.domain.ApiResponse
import io.ktor.client.HttpClient
import io.ktor.client.call.body
+import io.ktor.client.plugins.DefaultRequest
import io.ktor.client.plugins.HttpRequestRetry
+import io.ktor.client.plugins.HttpSend
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
+import io.ktor.client.plugins.plugin
+import io.ktor.client.request.header
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import io.ktor.http.ContentType
+import io.ktor.http.HttpHeaders
+import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import io.ktor.http.userAgent
import io.ktor.serialization.kotlinx.json.json
@@ -40,6 +47,7 @@ object KtorClient {
isLenient = true
ignoreUnknownKeys = true // JSON多了字段也不报错
encodeDefaults = false
+ explicitNulls = false
})
}
@@ -66,6 +74,64 @@ object KtorClient {
request.headers.append("X-Retry-Count", retryCount.toString())
}
}
+ /**
+ * 默认请求:
+ * - 统一加 User-Agent
+ * - 统一加 Authorization(如果有 Token)
+ */
+ install(DefaultRequest) {
+ userAgent("TaskTTL")
+
+ val token = AuthManager.currentAccessToken()
+ if (!token.isNullOrBlank()) {
+ header(HttpHeaders.Authorization, "Bearer $token")
+ }
+ }
+
+ install(HttpSend) {
+ maxSendCount = 2
+ }
+ }.also { client ->
+ // ⭐ 这里配置 HttpSend 拦截器:401 自动刷新 + 重试
+ client.plugin(HttpSend).intercept { request ->
+
+ // 发送请求前,先确保 token 是有效的(过期则刷新)
+ val validToken = AuthManager.getValidAccessToken()
+ if (!validToken.isNullOrBlank()) {
+ request.headers.remove(HttpHeaders.Authorization)
+ request.headers.append(HttpHeaders.Authorization, "Bearer $validToken")
+ }
+
+ // 先发一次请求
+ var call = execute(request)
+
+ // 如果不是 401,直接返回
+ if (call.response.status != HttpStatusCode.Unauthorized) {
+ return@intercept call
+ }
+
+ // 避免刷新接口自己 401 时死循环
+ val url = request.url.toString()
+ if (url.contains(ApiConfig.REFRESH_TOKEN_URL)) {
+ return@intercept call
+ }
+
+ // 尝试刷新 Token
+ val refreshed = AuthManager.tryRefreshToken()
+ if (!refreshed) {
+ return@intercept call
+ }
+
+ // 刷新成功后,重发原请求
+ val newToken = AuthManager.currentAccessToken()
+ if (!newToken.isNullOrBlank()) {
+ request.headers.remove(HttpHeaders.Authorization)
+ request.headers.append(HttpHeaders.Authorization, "Bearer $newToken")
+ }
+
+ // 再发一次(第二次)
+ execute(request)
+ }
}
/**
@@ -78,12 +144,11 @@ object KtorClient {
try {
val response = httpClient.post(url) {
contentType(ContentType.Application.Json)
- userAgent("TaskTTL")
setBody(body)
}
return handleResponse(response)
} catch (e: Exception) {
- throw Exception(e)
+ throw e
}
}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/divider/Divider.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/divider/Divider.kt
new file mode 100644
index 0000000..c21384d
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/divider/Divider.kt
@@ -0,0 +1,19 @@
+package com.taskttl.core.ui.component.divider
+
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+
+/**
+ * 水平分割线组件
+ *
+ * @param modifier 修饰符,用于自定义组件样式
+ * @param color 分割线颜色,默认使用outline颜色
+ */
+@Composable
+fun Divider(modifier: Modifier = Modifier, color: Color = MaterialTheme.colorScheme.outline) {
+ HorizontalDivider(modifier, thickness = 0.5.dp, color = color)
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/Empty.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/Empty.kt
new file mode 100644
index 0000000..55e6cd0
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/Empty.kt
@@ -0,0 +1,121 @@
+package com.taskttl.core.ui.component.empty
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.taskttl.core.designsystem.component.SpaceVerticalSmall
+import com.taskttl.core.designsystem.component.SpaceVerticalXLarge
+import com.taskttl.core.designsystem.theme.SpacePaddingLarge
+import com.taskttl.core.designsystem.theme.AppTheme
+import org.jetbrains.compose.resources.DrawableResource
+import org.jetbrains.compose.resources.StringResource
+import org.jetbrains.compose.resources.stringResource
+import org.jetbrains.compose.resources.vectorResource
+import taskttl.composeapp.generated.resources.Res
+import taskttl.composeapp.generated.resources.click_retry
+import taskttl.composeapp.generated.resources.empty_error
+import taskttl.composeapp.generated.resources.ic_empty_error
+
+/**
+ * 页面状态视图
+ *
+ * @param modifier 修饰符
+ * @param message 消息文本资源ID
+ * @param subtitle 副标题文本资源ID
+ * @param retryButtonText 重试按钮文本资源ID
+ * @param icon 图标资源ID
+ * @param onRetryClick 重试点击回调
+
+ */
+@Composable
+fun Empty(
+ modifier: Modifier = Modifier,
+ message: StringResource = Res.string.empty_error,
+ subtitle: StringResource? = null,
+ retryButtonText: StringResource = Res.string.click_retry,
+ icon: DrawableResource = Res.drawable.ic_empty_error,
+ onRetryClick: (() -> Unit)? = null,
+) {
+ Column(
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = modifier
+ .fillMaxSize()
+ .padding(SpacePaddingLarge)
+ ) {
+ Icon(
+ imageVector = vectorResource(icon),
+ modifier = Modifier.size(120.dp),
+ tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2F),
+ contentDescription = null,
+ )
+
+ SpaceVerticalXLarge()
+
+ Text(
+ text = stringResource(message),
+ style = MaterialTheme.typography.titleLarge
+ )
+
+ subtitle?.let {
+ SpaceVerticalSmall()
+ Text(
+ text = stringResource(it),
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+
+ // 如果没有传递重试方法,则不显示重试按钮
+ if (onRetryClick != null) {
+ SpaceVerticalXLarge()
+ OutlinedButton(
+ onClick = onRetryClick,
+ border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.primary),
+ modifier = Modifier
+ .padding(horizontal = 50.dp)
+ .widthIn(200.dp)
+ ) {
+ Text(
+ text = stringResource(retryButtonText)
+ )
+ }
+ }
+ }
+}
+
+/**
+ * 页面状态视图浅色主题预览
+ *
+ */
+@Preview(showBackground = true)
+@Composable
+fun EmptyPreview() {
+ AppTheme {
+ Empty()
+ }
+}
+
+/**
+ * 页面状态视图深色主题预览
+ *
+ */
+@Preview(showBackground = true)
+@Composable
+fun EmptyPreviewDark() {
+ AppTheme(darkTheme = true) {
+ Empty()
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyData.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyData.kt
new file mode 100644
index 0000000..2a69269
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyData.kt
@@ -0,0 +1,36 @@
+package com.taskttl.core.ui.component.empty
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import com.taskttl.core.designsystem.theme.AppTheme
+import taskttl.composeapp.generated.resources.Res
+import taskttl.composeapp.generated.resources.click_retry
+import taskttl.composeapp.generated.resources.empty_data
+import taskttl.composeapp.generated.resources.ic_empty_data
+import taskttl.composeapp.generated.resources.text_add_task_hint
+import taskttl.composeapp.generated.resources.text_no_tasks
+
+@Composable
+fun EmptyData(modifier: Modifier, onRetryClick: (() -> Unit)? = null) {
+ Empty(
+ message = Res.string.text_no_tasks,
+ subtitle = Res.string.text_add_task_hint
+ )
+}
+
+/**
+ * 暂无数据状态预览
+ *
+ */
+@Preview(showBackground = true)
+@Composable
+fun EmptyDataPreview() {
+ AppTheme {
+ Empty(
+ message = Res.string.empty_data,
+ icon = Res.drawable.ic_empty_data,
+ retryButtonText = Res.string.click_retry,
+ )
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyError.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyError.kt
new file mode 100644
index 0000000..2a193fe
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyError.kt
@@ -0,0 +1,49 @@
+package com.taskttl.core.ui.component.empty
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import com.taskttl.core.designsystem.theme.AppTheme
+import taskttl.composeapp.generated.resources.Res
+import taskttl.composeapp.generated.resources.click_retry
+import taskttl.composeapp.generated.resources.empty_error
+import taskttl.composeapp.generated.resources.empty_error_subtitle
+import taskttl.composeapp.generated.resources.ic_empty_error
+
+/**
+ * 加载失败状态视图
+ *
+ * @param modifier 修饰符
+ * @param onRetryClick 重试点击回调
+ */
+@Composable
+fun EmptyError(
+ modifier: Modifier = Modifier,
+ onRetryClick: (() -> Unit)? = null,
+) {
+ Empty(
+ modifier = modifier,
+ message = Res.string.empty_error,
+ subtitle = Res.string.empty_error_subtitle,
+ icon = Res.drawable.ic_empty_error,
+ retryButtonText = Res.string.click_retry,
+ onRetryClick = onRetryClick
+ )
+}
+
+/**
+ * 加载失败状态预览
+ *
+
+ */
+@Preview(showBackground = true)
+@Composable
+fun EmptyErrorPreview() {
+ AppTheme {
+ Empty(
+ message = Res.string.empty_error,
+ icon = Res.drawable.ic_empty_error,
+ retryButtonText = Res.string.click_retry,
+ )
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyNetwork.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyNetwork.kt
new file mode 100644
index 0000000..e0eb2ba
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/ui/component/empty/EmptyNetwork.kt
@@ -0,0 +1,48 @@
+package com.taskttl.core.ui.component.empty
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import com.taskttl.core.designsystem.theme.AppTheme
+import taskttl.composeapp.generated.resources.Res
+import taskttl.composeapp.generated.resources.click_retry
+import taskttl.composeapp.generated.resources.empty_network
+import taskttl.composeapp.generated.resources.empty_network_subtitle
+import taskttl.composeapp.generated.resources.ic_empty_network
+
+/**
+ * 网络连接失败状态视图
+ *
+ * @param modifier 修饰符
+ * @param onRetryClick 重试点击回调
+ */
+@Composable
+fun EmptyNetwork(
+ modifier: Modifier = Modifier,
+ onRetryClick: (() -> Unit)? = null,
+) {
+ Empty(
+ modifier = modifier,
+ message = Res.string.empty_network,
+ subtitle = Res.string.empty_network_subtitle,
+ icon = Res.drawable.ic_empty_network,
+ retryButtonText = Res.string.click_retry,
+ onRetryClick = onRetryClick
+ )
+}
+
+/**
+ * 网络连接失败状态预览
+ *
+ */
+@Preview(showBackground = true)
+@Composable
+fun EmptyNetworkPreview() {
+ AppTheme {
+ Empty(
+ message = Res.string.empty_network,
+ icon = Res.drawable.ic_empty_network,
+ retryButtonText = Res.string.click_retry,
+ )
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/StorageUtils.kt b/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/StorageUtils.kt
index 6373cf4..08b5579 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/StorageUtils.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/core/utils/StorageUtils.kt
@@ -2,8 +2,7 @@ package com.taskttl.core.utils
import com.russhwolf.settings.Settings
import com.russhwolf.settings.contains
-import com.russhwolf.settings.get
-import com.russhwolf.settings.set
+import kotlinx.serialization.json.Json
/**
@@ -13,14 +12,19 @@ import com.russhwolf.settings.set
*/
object StorageUtils {
val settings: Settings = Settings()
+
+ val json = Json {
+ ignoreUnknownKeys = true
+ encodeDefaults = true
+ explicitNulls = false
+ }
+
/**
* 保存字符串值
* @param key 键
* @param value 值
*/
- fun saveString(key: String, value: String) {
- settings.putString(key,value)
- }
+ fun saveString(key: String, value: String) = settings.putString(key, value)
/**
* 获取字符串值
@@ -28,18 +32,14 @@ object StorageUtils {
* @param defaultValue 默认值
* @return 存储的字符串值或默认值
*/
- fun getString(key: String, defaultValue: String = ""): String {
- return settings.getString(key,defaultValue)
- }
+ fun getString(key: String, defaultValue: String = "") = settings.getString(key, defaultValue)
/**
* 保存整数值
* @param key 键
* @param value 值
*/
- fun saveInt(key: String, value: Int) {
- settings.putInt(key,value)
- }
+ fun saveInt(key: String, value: Int) = settings.putInt(key, value)
/**
* 获取整数值
@@ -47,9 +47,7 @@ object StorageUtils {
* @param defaultValue 默认值
* @return 存储的整数值或默认值
*/
- fun getInt(key: String, defaultValue: Int = 0): Int {
- return settings.getInt(key,defaultValue)
- }
+ fun getInt(key: String, defaultValue: Int = 0) = settings.getInt(key, defaultValue)
/**
@@ -57,9 +55,7 @@ object StorageUtils {
* @param [key] 键
* @param [value] 值
*/
- fun saveLong(key: String, value: Long) {
- settings.putLong(key,value)
- }
+ fun saveLong(key: String, value: Long) = settings.putLong(key, value)
/**
* 获取长整数值
@@ -67,18 +63,14 @@ object StorageUtils {
* @param defaultValue 默认值
* @return 存储的长整数值或默认值
*/
- fun getLong(key: String, defaultValue: Long): Long {
- return settings.getLong(key,defaultValue)
- }
+ fun getLong(key: String, defaultValue: Long = 0L) = settings.getLong(key, defaultValue)
/**
* 保存布尔值
* @param key 键
* @param value 值
*/
- fun saveBoolean(key: String, value: Boolean) {
- settings.putBoolean(key,value)
- }
+ fun saveBoolean(key: String, value: Boolean) = settings.putBoolean(key, value)
/**
* 获取布尔值
@@ -86,18 +78,16 @@ object StorageUtils {
* @param defaultValue 默认值
* @return 存储的布尔值或默认值
*/
- fun getBoolean(key: String, defaultValue: Boolean = false): Boolean {
- return settings.getBoolean(key,defaultValue)
- }
+ fun getBoolean(key: String, defaultValue: Boolean = false) =
+ settings.getBoolean(key, defaultValue)
/**
* 保存对象
* @param key 键
* @param value 对象
*/
- inline fun saveObject(key: String, value: T) {
- settings[key] = value
- }
+ inline fun saveObject(key: String, value: T) =
+ settings.putString(key, json.encodeToString(value))
/**
* 获取对象
@@ -105,7 +95,8 @@ object StorageUtils {
* @return 存储的对象或null
*/
inline fun getObject(key: String): T? {
- return settings[key]
+ val stored = settings.getStringOrNull(key) ?: return null
+ return runCatching { json.decodeFromString(stored) }.getOrNull()
}
/**
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/constant/Constant.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/constant/Constant.kt
index 22729ca..636b191 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/constant/Constant.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/constant/Constant.kt
@@ -17,4 +17,11 @@ object Constant {
const val WEB_CLIENT_ID =
"649192447921-rtn0jklurc7cr4oalh9gh3684mnlklce.apps.googleusercontent.com"
+
+ /** 密钥令牌信息 */
+ const val KEY_TOKEN_INFO = "tokenInfo"
+
+ /** 关键用户信息 */
+ const val KEY_USER_INFO = "userInfo"
+
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/repository/CountdownRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/repository/CountdownRepositoryImpl.kt
index b6494c6..91b9a56 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/repository/CountdownRepositoryImpl.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/repository/CountdownRepositoryImpl.kt
@@ -63,6 +63,17 @@ class CountdownRepositoryImpl(
countdownDao.deleteCountdown(id)
}
+ override suspend fun clearExpiredCountdowns() {
+ countdownDao.clearExpiredCountdowns()
+ }
+
+ /**
+ * 删除所有倒计时
+ */
+ override suspend fun deleteAllCountdowns() {
+ countdownDao.deleteAllCountdowns()
+ }
+
/**
* 获取倒数日通过类别
* @param [category] 类别
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/repository/TaskRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/repository/TaskRepositoryImpl.kt
index e8f53ef..53c1ade 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/repository/TaskRepositoryImpl.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/repository/TaskRepositoryImpl.kt
@@ -63,6 +63,20 @@ class TaskRepositoryImpl(
taskDao.deleteTask(id)
}
+ /**
+ * 删除已完成任务
+ */
+ override suspend fun deleteCompletedTasks() {
+ taskDao.deleteCompletedTasks()
+ }
+
+ /**
+ * 删除所有任务
+ */
+ override suspend fun deleteAllTasks() {
+ taskDao.deleteAllTasks()
+ }
+
/**
* 按类别获取任务
* @param [categoryId] 类别
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/CategoryDao.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/CategoryDao.kt
index 5d99235..c24c929 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/CategoryDao.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/CategoryDao.kt
@@ -81,13 +81,13 @@ interface CategoryDao {
/**
* 更新任务计数
*/
- @Query("UPDATE categories SET taskCount = (SELECT COUNT(*) FROM tasks WHERE category = categories.name) WHERE type = 'TASK'")
+ @Query("UPDATE categories SET taskCount = (SELECT COUNT(*) FROM tasks WHERE category = categories.id) WHERE type = 'TASK'")
suspend fun updateTaskCounts()
/**
* 更新倒计时计数
*/
- @Query("UPDATE categories SET countdownCount = (SELECT COUNT(*) FROM countdowns WHERE category = categories.name) WHERE type = 'COUNTDOWN'")
+ @Query("UPDATE categories SET countdownCount = (SELECT COUNT(*) FROM countdowns WHERE category = categories.id) WHERE type = 'COUNTDOWN'")
suspend fun updateCountdownCounts()
/**
@@ -100,10 +100,10 @@ interface CategoryDao {
COALESCE(t.task_count, 0) as taskCount,
COALESCE(cd.countdown_count, 0) as countdownCount
FROM categories c
- LEFT JOIN (SELECT category, COUNT(*) as task_count FROM tasks GROUP BY category) t ON c.name = t.category
- LEFT JOIN (SELECT category, COUNT(*) as countdown_count FROM countdowns GROUP BY category) cd ON c.name = cd.category
+ LEFT JOIN (SELECT category, COUNT(*) as task_count FROM tasks GROUP BY category) t ON c.id = t.category
+ LEFT JOIN (SELECT category, COUNT(*) as countdown_count FROM countdowns GROUP BY category) cd ON c.id = cd.category
WHERE c.type = :type
ORDER BY c.createdAt ASC
""")
fun getCategoriesWithCounts(type: String): Flow>
-}
\ No newline at end of file
+}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/CountdownDao.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/CountdownDao.kt
index 53b0460..acc21bc 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/CountdownDao.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/CountdownDao.kt
@@ -60,6 +60,13 @@ interface CountdownDao {
@Query("DELETE FROM countdowns WHERE id = :id")
suspend fun deleteCountdown(id: String)
+
+ /**
+ * 清除过期倒计时
+ */
+ @Query("DELETE FROM countdowns WHERE isActive = false")
+ suspend fun clearExpiredCountdowns()
+
/**
* 删除所有倒数日
*/
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/TaskDao.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/TaskDao.kt
index a27552e..46174c5 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/TaskDao.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/local/dao/TaskDao.kt
@@ -74,6 +74,12 @@ interface TaskDao {
@Query("DELETE FROM tasks WHERE id = :id")
suspend fun deleteTask(id: String)
+ /**
+ * 删除已完成任务
+ */
+ @Query("DELETE FROM tasks WHERE isCompleted = true")
+ suspend fun deleteCompletedTasks()
+
/**
* 删除所有任务
*/
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/api/TaskTTLApi.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/api/TaskTTLApi.kt
index 6eadde8..da46f54 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/api/TaskTTLApi.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/api/TaskTTLApi.kt
@@ -3,15 +3,21 @@ package com.taskttl.data.source.remote.api
import com.taskttl.core.domain.constant.PointEvent
import com.taskttl.core.domain.toJson
import com.taskttl.core.network.ApiConfig
+import com.taskttl.core.network.AuthHttpClient
import com.taskttl.core.network.KtorClient
import com.taskttl.core.utils.DeviceUtils
-import com.taskttl.core.utils.LogUtils
+import com.taskttl.data.source.remote.api.TaskTTLApi.refreshToken
import com.taskttl.data.source.remote.dto.request.FeedbackReq
-import com.taskttl.data.source.remote.dto.request.LoginReq
import com.taskttl.data.source.remote.dto.request.PointReq
+import com.taskttl.data.source.remote.dto.request.RefreshTokenRequest
+import com.taskttl.data.source.remote.dto.request.ThirdPartyLoginReq
import com.taskttl.data.source.remote.dto.response.Account
-import com.taskttl.data.source.remote.dto.response.AuthResult
import com.taskttl.data.source.remote.dto.response.FeedbackResp
+import com.taskttl.data.source.remote.dto.response.ThirdPartyLoginResp
+import io.ktor.client.request.post
+import io.ktor.client.request.setBody
+import io.ktor.http.ContentType
+import io.ktor.http.contentType
import org.jetbrains.compose.resources.getString
import taskttl.composeapp.generated.resources.Res
import taskttl.composeapp.generated.resources.feedback_error
@@ -21,12 +27,31 @@ object TaskTTLApi {
/**
* 第三方登录
* @param [account] 账户
- * @return [FeedbackResp]
+ * @return [ThirdPartyLoginResp]
*/
- suspend fun thirdPartyLogin(account: Account): FeedbackResp {
+ suspend fun thirdPartyLogin(account: Account): ThirdPartyLoginResp {
try {
- val loginReq = LoginReq(account.id,account.idToken,account.provider.name)
- return KtorClient.postJson(ApiConfig.THIRD_PARTY_LOGIN_URL,loginReq.toJson())
+ val thirdPartyLoginReq = ThirdPartyLoginReq.fromAccount(account)
+ thirdPartyLoginReq.baseReq = DeviceUtils.getDeviceInfo()
+ return KtorClient.postJson(ApiConfig.THIRD_PARTY_LOGIN_URL, thirdPartyLoginReq.toJson())
+ } catch (_: Exception) {
+ throw Exception(getString(Res.string.feedback_error))
+ }
+ }
+
+ /**
+ * 刷新令牌
+ * @param [refreshToken] 刷新令牌
+ * @return [ThirdPartyLoginResp]
+ */
+ suspend fun refreshToken(refreshToken: RefreshTokenRequest): ThirdPartyLoginResp {
+ try {
+ refreshToken.baseReq = DeviceUtils.getDeviceInfo()
+ val response = AuthHttpClient.client.post(ApiConfig.REFRESH_TOKEN_URL) {
+ contentType(ContentType.Application.Json)
+ setBody(refreshToken.toJson())
+ }
+ return KtorClient.handleResponse(response)
} catch (_: Exception) {
throw Exception(getString(Res.string.feedback_error))
}
@@ -41,7 +66,7 @@ object TaskTTLApi {
try {
feedbackReq.baseReq = DeviceUtils.getDeviceInfo()
return KtorClient.postJson(ApiConfig.FEEDBACK_URL, feedbackReq.toJson())
- } catch (e: Exception) {
+ } catch (_: Exception) {
throw Exception(getString(Res.string.feedback_error))
}
}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/LoginReq.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/LoginReq.kt
deleted file mode 100644
index 9eb19ec..0000000
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/LoginReq.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.taskttl.data.source.remote.dto.request
-
-import com.taskttl.core.domain.BaseReqWith
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-
-/**
- * 登录请求
- * @author DevTTL
- * @date 2025/11/13
- * @constructor 创建[LoginReq]
- * @param [id] id
- * @param [idToken] id令牌
- * @param [provider] 提供者
- */
-@Serializable
-data class LoginReq(
- @SerialName("id")
- val id: String,
- @SerialName("idToken")
- val idToken: String,
- @SerialName("provider")
- val provider: String,
-) : BaseReqWith()
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/RefreshTokenRequest.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/RefreshTokenRequest.kt
new file mode 100644
index 0000000..13a2a98
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/RefreshTokenRequest.kt
@@ -0,0 +1,16 @@
+package com.taskttl.data.source.remote.dto.request
+
+import com.taskttl.core.domain.BaseReqWith
+import kotlinx.serialization.Serializable
+
+/**
+ * 刷新令牌请求
+ * @author admin
+ * @date 2025/11/26
+ * @constructor 创建[RefreshTokenRequest]
+ * @param [refreshToken] 刷新令牌
+ */
+@Serializable
+class RefreshTokenRequest(
+ val refreshToken: String
+) : BaseReqWith()
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/ThirdPartyLoginReq.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/ThirdPartyLoginReq.kt
new file mode 100644
index 0000000..13a77f8
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/request/ThirdPartyLoginReq.kt
@@ -0,0 +1,39 @@
+package com.taskttl.data.source.remote.dto.request
+
+import com.taskttl.core.domain.BaseReqWith
+import com.taskttl.data.source.remote.dto.response.Account
+import kotlinx.serialization.Serializable
+
+/**
+ * 第三聚会登录请求
+ * @author admin
+ * @date 2025/11/23
+ * @constructor 创建[ThirdPartyLoginReq]
+ * @param [provider] 提供者
+ * @param [idToken] ID令牌
+ * @param [baseReq] 基础请求
+ */
+@Serializable
+class ThirdPartyLoginReq(
+ val id: String,
+ val provider: String,
+ val idToken: String,
+) : BaseReqWith() {
+
+ companion object {
+
+ /**
+ * 获取通过账户
+ * @param [account] 账户
+ * @return [ThirdPartyLoginReq]
+ */
+ fun fromAccount(account: Account): ThirdPartyLoginReq {
+ return ThirdPartyLoginReq(
+ id = account.id,
+ provider = account.provider.name,
+ idToken = account.idToken
+ )
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/AuthResult.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/AuthResult.kt
index c20f8aa..f9351ae 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/AuthResult.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/AuthResult.kt
@@ -2,6 +2,12 @@ package com.taskttl.data.source.remote.dto.response
import com.taskttl.data.constant.ProviderEnum
+/**
+ * 身份验证结果
+ * @author admin
+ * @date 2025/11/25
+ * @constructor 创建[AuthResult]
+ */
sealed class AuthResult {
data class Success(val account: Account) : AuthResult()
data class Error(val message: String?) : AuthResult()
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/RefreshTokenResponse.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/RefreshTokenResponse.kt
new file mode 100644
index 0000000..bc58cf1
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/RefreshTokenResponse.kt
@@ -0,0 +1,32 @@
+package com.taskttl.data.source.remote.dto.response
+
+import com.taskttl.core.auth.TokenInfo
+import kotlinx.serialization.Serializable
+
+/**
+ * 刷新令牌响应
+ * @author admin
+ * @date 2025/11/26
+ * @constructor 创建[RefreshTokenResponse]
+ * @param [accessToken] 访问令牌
+ * @param [refreshToken] 刷新令牌
+ * @param [expiresIn] 到期时间剩余
+ */
+@Serializable
+data class RefreshTokenResponse(
+ val accessToken: String, // 访问令牌
+ val refreshToken: String, // 刷新令牌
+ val expiresIn: Long // 到期时间剩余
+) {
+ /**
+ * 转成令牌信息
+ * @return [TokenInfo]
+ */
+ fun toTokenInfo(): TokenInfo {
+ return TokenInfo(
+ accessToken = accessToken,
+ refreshToken = refreshToken,
+ expiresAt = expiresIn
+ )
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/ThirdPartyLoginResp.kt b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/ThirdPartyLoginResp.kt
new file mode 100644
index 0000000..9f69c06
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/data/source/remote/dto/response/ThirdPartyLoginResp.kt
@@ -0,0 +1,55 @@
+package com.taskttl.data.source.remote.dto.response
+
+import com.taskttl.core.auth.TokenInfo
+import com.taskttl.domain.model.UserInfo
+import kotlinx.serialization.Serializable
+
+/**
+ * 第三方登录响应
+ * @author admin
+ * @date 2025/11/25
+ * @constructor 创建[ThirdPartyLoginResp]
+ * @param [userId] 用户ID
+ * @param [nickname] 昵称
+ * @param [avatar] 头像
+ * @param [email] 电子邮件
+ * @param [accessToken] 访问令牌
+ * @param [refreshToken] 刷新令牌
+ * @param [expiresAt] 到期日
+ */
+@Serializable
+data class ThirdPartyLoginResp(
+ var userId: Long, // 用户ID
+ var nickname: String, // 昵称
+ var avatar: String, // 头像
+ var email: String, // 电子邮件
+ var accessToken: String, // 访问令牌
+ var refreshToken: String, // 刷新令牌
+ var expiresAt: Long, // 到期日
+) {
+ /**
+ * 转成令牌信息
+ * @return [TokenInfo]
+ */
+ fun toTokenInfo(): TokenInfo {
+ return TokenInfo(
+ accessToken = accessToken,
+ refreshToken = refreshToken,
+ expiresAt = expiresAt
+ )
+ }
+
+ /**
+ * 转成用户信息
+ * @return [UserInfo]
+ */
+ fun toUserInfo(): UserInfo {
+ return UserInfo(
+ userId = userId,
+ nickname = nickname,
+ avatar = avatar,
+ email = email
+ )
+ }
+}
+
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/domain/model/UserInfo.kt b/composeApp/src/commonMain/kotlin/com/taskttl/domain/model/UserInfo.kt
new file mode 100644
index 0000000..29dc3e8
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/domain/model/UserInfo.kt
@@ -0,0 +1,21 @@
+package com.taskttl.domain.model
+
+import kotlinx.serialization.Serializable
+
+/**
+ * 用户信息
+ * @author admin
+ * @date 2025/11/23
+ * @constructor 创建[UserInfo]
+ * @param [userId] 用户ID
+ * @param [nickname] 昵称
+ * @param [avatar] 头像
+ * @param [email] 电子邮件
+ */
+@Serializable
+data class UserInfo(
+ var userId: Long, // 用户ID
+ var nickname: String, // 昵称
+ var avatar: String, // 头像
+ var email: String, // 电子邮件
+)
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/CountdownRepository.kt b/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/CountdownRepository.kt
index f4b6edb..c9ffdb7 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/CountdownRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/CountdownRepository.kt
@@ -41,6 +41,16 @@ interface CountdownRepository {
*/
suspend fun deleteCountdown(id: String)
+ /**
+ * 清除过期倒计时
+ */
+ suspend fun clearExpiredCountdowns()
+
+ /**
+ * 删除所有倒计时
+ */
+ suspend fun deleteAllCountdowns()
+
/**
* 获取倒数日通过类别
* @param [category] 类别
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/TaskRepository.kt b/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/TaskRepository.kt
index 04e13c9..ba5d14a 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/TaskRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/domain/repository/TaskRepository.kt
@@ -41,6 +41,16 @@ interface TaskRepository {
*/
suspend fun deleteTask(id: String)
+ /**
+ * 删除已完成任务
+ */
+ suspend fun deleteCompletedTasks()
+
+ /**
+ * 删除所有任务
+ */
+ suspend fun deleteAllTasks()
+
/**
* 按类别获取任务
* @param [category] 类别
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/navigation/MainNav.kt b/composeApp/src/commonMain/kotlin/com/taskttl/navigation/MainNav.kt
index 30e4190..4877dc4 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/navigation/MainNav.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/navigation/MainNav.kt
@@ -1,5 +1,7 @@
package com.taskttl.navigation
// 主导航
+import androidx.compose.animation.AnimatedContentTransitionScope
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
@@ -23,7 +25,6 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.taskttl.presentation.common.components.CustomBottomBar
-import com.taskttl.presentation.features.auth.LoginScreen
import com.taskttl.presentation.features.category.editor.CategoryEditScreen
import com.taskttl.presentation.features.category.list.CategoryScreen
import com.taskttl.presentation.features.countdown.detail.CountdownDetailScreen
@@ -38,6 +39,8 @@ import com.taskttl.presentation.features.statistics.StatisticsScreen
import com.taskttl.presentation.features.task.detail.TaskEditorScreen
import com.taskttl.presentation.features.task.editor.TaskDetailScreen
import com.taskttl.presentation.features.task.list.TaskScreen
+import com.taskttl.presentation.features.user.login.LoginScreen
+import com.taskttl.presentation.features.user.userProfile.UserProfileScreen
import taskttl.composeapp.generated.resources.Res
import taskttl.composeapp.generated.resources.nav_countdown
import taskttl.composeapp.generated.resources.nav_settings
@@ -85,6 +88,34 @@ fun MainNav() {
NavHost(
modifier = Modifier.fillMaxSize()
.padding(bottom = paddingValues.calculateBottomPadding()),
+ // 页面进入动画
+ enterTransition = {
+ slideIntoContainer(
+ towards = AnimatedContentTransitionScope.SlideDirection.Left,
+ animationSpec = tween(300)
+ )
+ },
+ // 页面退出动画
+ exitTransition = {
+ slideOutOfContainer(
+ towards = AnimatedContentTransitionScope.SlideDirection.Left,
+ animationSpec = tween(300)
+ )
+ },
+ // 返回时页面进入动画
+ popEnterTransition = {
+ slideIntoContainer(
+ towards = AnimatedContentTransitionScope.SlideDirection.Right,
+ animationSpec = tween(300)
+ )
+ },
+ // 返回时页面退出动画
+ popExitTransition = {
+ slideOutOfContainer(
+ towards = AnimatedContentTransitionScope.SlideDirection.Right,
+ animationSpec = tween(300)
+ )
+ },
navController = mainNavController,
startDestination = Routes.Main.Task
) {
@@ -109,7 +140,13 @@ fun MainNav() {
TaskDetailScreen(
taskId = taskDetail.taskId,
onNavigateBack = { mainNavController.popBackStack() },
- onNavigateToEdit = { mainNavController.navigate(Routes.Main.Task.EditTask(taskDetail.taskId)) }
+ onNavigateToEdit = {
+ mainNavController.navigate(
+ Routes.Main.Task.EditTask(
+ taskDetail.taskId
+ )
+ )
+ }
)
}
@@ -185,11 +222,16 @@ fun MainNav() {
)
}
- composable {
+ composable {
LoginScreen(
onNavigateBack = { mainNavController.popBackStack() }
)
}
+ composable {
+ UserProfileScreen(
+ onNavigateBack = { mainNavController.popBackStack() }
+ )
+ }
}
}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/navigation/Routes.kt b/composeApp/src/commonMain/kotlin/com/taskttl/navigation/Routes.kt
index 784d6df..fdf1839 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/navigation/Routes.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/navigation/Routes.kt
@@ -174,13 +174,31 @@ sealed interface Routes {
@Serializable
data object About : Routes
- /**
- * 登录
- * @author DevTTL
- * @date 2025/11/07
- */
- @Serializable
- data object Login : Routes
+
}
}
+
+ /**
+ * 用户
+ * @author DevTTL
+ * @date 2025/12/03
+ */
+ @Serializable
+ data object User : Routes {
+ /**
+ * 登录
+ * @author DevTTL
+ * @date 2025/11/07
+ */
+ @Serializable
+ data object Login : Routes
+
+ /**
+ * 用户档案
+ * @author DevTTL
+ * @date 2025/12/03
+ */
+ @Serializable
+ data object UserProfile: Routes
+ }
}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/ActionButtonListItem.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/ActionButtonListItem.kt
index ee7eaf0..84aab34 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/ActionButtonListItem.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/ActionButtonListItem.kt
@@ -4,7 +4,6 @@ import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
@@ -36,6 +35,7 @@ import androidx.compose.ui.UiComposable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@@ -46,7 +46,6 @@ import kotlin.math.sign
private val PaddingSm = 8.dp
-@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ActionButtonListItem(
modifier: Modifier = Modifier,
@@ -282,6 +281,7 @@ private fun applyDamping(
}
}
+@Preview
@Composable
private fun ActionButtonListItemPreview() {
var isOpen by remember { mutableStateOf(false) }
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/NetworkImage.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/NetworkImage.kt
index b20c6be..4494106 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/NetworkImage.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/components/NetworkImage.kt
@@ -7,6 +7,7 @@ import coil3.compose.AsyncImage
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.request.crossfade
+import com.taskttl.core.utils.LogUtils
import org.jetbrains.compose.resources.painterResource
import taskttl.composeapp.generated.resources.Res
import taskttl.composeapp.generated.resources.ic_launcher
@@ -23,6 +24,14 @@ fun NetworkImage(
modifier = modifier,
model = ImageRequest.Builder(context)
.data(url)
+ .listener(
+ onError = { _, result ->
+ LogUtils.e("DevTTL_CoilError", "Load fail: ${result.throwable}")
+ },
+ onSuccess = { _, _ ->
+ LogUtils.d("DevTTL_Coil", "Load success")
+ }
+ )
// 缓存策略:启用内存 + 磁盘缓存
.memoryCachePolicy(CachePolicy.ENABLED)
.diskCachePolicy(CachePolicy.ENABLED)
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/foundation/GlobalImageLoader.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/foundation/GlobalImageLoader.kt
index ecf5c07..c0d2e06 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/foundation/GlobalImageLoader.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/foundation/GlobalImageLoader.kt
@@ -7,7 +7,9 @@ import coil3.SingletonImageLoader
import coil3.compose.LocalPlatformContext
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
+import coil3.network.ktor3.KtorNetworkFetcherFactory
import coil3.request.crossfade
+import com.taskttl.core.network.KtorClient
import okio.Path
/**
@@ -19,6 +21,7 @@ fun GlobalImageLoader() {
remember(platformContext) {
val imageLoader = ImageLoader.Builder(platformContext)
+ .components { add(KtorNetworkFetcherFactory(KtorClient.httpClient)) }
// 内存缓存
.memoryCache {
MemoryCache.Builder()
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/theme/Theme.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/theme/Theme.kt
deleted file mode 100644
index 5022eaa..0000000
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/theme/Theme.kt
+++ /dev/null
@@ -1,139 +0,0 @@
-package com.taskttl.ui.theme
-
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.darkColorScheme
-import androidx.compose.material3.lightColorScheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.graphics.Color
-
-/** 浅色方案 - 优化后的配色方案(Material Design 3风格)*/
-private val LightColorScheme = lightColorScheme(
- // 主色调 - 更鲜艳的紫蓝色系,提升视觉冲击力
- primary = Color(0xFF5B6FF8),
- onPrimary = Color(0xFFFFFFFF),
- primaryContainer = Color(0xFFE1E4FF),
- onPrimaryContainer = Color(0xFF001A41),
-
- // 次要色 - 优化紫色系,更柔和协调
- secondary = Color(0xFF8B5FBF),
- onSecondary = Color(0xFFFFFFFF),
- secondaryContainer = Color(0xFFF3E8FF),
- onSecondaryContainer = Color(0xFF2C0051),
-
- // 第三色 - 温暖的橙色系(用于倒计时等)
- tertiary = Color(0xFFFF9500),
- onTertiary = Color(0xFFFFFFFF),
- tertiaryContainer = Color(0xFFFFE6CC),
- onTertiaryContainer = Color(0xFF331F00),
-
- // 错误色 - 更现代的红色系
- error = Color(0xFFDC3545),
- onError = Color(0xFFFFFFFF),
- errorContainer = Color(0xFFFFE5E5),
- onErrorContainer = Color(0xFF5F0010),
-
- // 背景色 - 更柔和的浅色背景
- background = Color(0xFFFAFBFC),
- onBackground = Color(0xFF1A1C1E),
-
- // 表面色 - 纯净白色
- surface = Color(0xFFFFFFFF),
- onSurface = Color(0xFF1A1C1E),
- surfaceVariant = Color(0xFFE4E6EB),
- onSurfaceVariant = Color(0xFF45464F),
-
- // 轮廓色 - 更细腻的边框
- outline = Color(0xFFCED0D6),
- outlineVariant = Color(0xFFE4E6EB),
-
- // 其他
- scrim = Color(0xFF000000),
- inverseSurface = Color(0xFF2F3033),
- inverseOnSurface = Color(0xFFF1F0F4),
- inversePrimary = Color(0xFFAAB4FF),
-
- // 表面容器色 - 更丰富的层次
- surfaceDim = Color(0xFFDBDCE0),
- surfaceBright = Color(0xFFFFFFFF),
- surfaceContainerLowest = Color(0xFFFFFFFF),
- surfaceContainerLow = Color(0xFFF5F6FA),
- surfaceContainer = Color(0xFFEFF0F5),
- surfaceContainerHigh = Color(0xFFE9EAF0),
- surfaceContainerHighest = Color(0xFFE3E4EA)
-)
-
-/** 深色配色方案 - 优化后的深色模式(Material Design 3风格)*/
-private val DarkColorScheme = darkColorScheme(
- // 主色调 - 柔和亮丽的紫蓝色系
- primary = Color(0xFFAAB4FF),
- onPrimary = Color(0xFF001A41),
- primaryContainer = Color(0xFF3D4DB8),
- onPrimaryContainer = Color(0xFFE1E4FF),
-
- // 次要色 - 优雅的亮紫色系
- secondary = Color(0xFFD4BCFF),
- onSecondary = Color(0xFF2C0051),
- secondaryContainer = Color(0xFF5A3D85),
- onSecondaryContainer = Color(0xFFF3E8FF),
-
- // 第三色 - 温暖的亮橙色系
- tertiary = Color(0xFFFFBB5C),
- onTertiary = Color(0xFF331F00),
- tertiaryContainer = Color(0xFFCC7700),
- onTertiaryContainer = Color(0xFFFFE6CC),
-
- // 错误色 - 柔和的亮红色系
- error = Color(0xFFFF7782),
- onError = Color(0xFF5F0010),
- errorContainer = Color(0xFFB42734),
- onErrorContainer = Color(0xFFFFE5E5),
-
- // 背景色 - OLED友好的纯黑
- background = Color(0xFF1A1C1E),
- onBackground = Color(0xFFE3E2E6),
-
- // 表面色 - 稍亮的深色
- surface = Color(0xFF1A1C1E),
- onSurface = Color(0xFFE3E2E6),
- surfaceVariant = Color(0xFF45464F),
- onSurfaceVariant = Color(0xFFC5C6D0),
-
- // 轮廓色 - 更好的边框可见度
- outline = Color(0xFF8F9099),
- outlineVariant = Color(0xFF45464F),
-
- // 其他
- scrim = Color(0xFF000000),
- inverseSurface = Color(0xFFE3E2E6),
- inverseOnSurface = Color(0xFF2F3033),
- inversePrimary = Color(0xFF5B6FF8),
-
- // 表面容器色 - 深色模式下的精细层次
- surfaceDim = Color(0xFF121316),
- surfaceBright = Color(0xFF393B3F),
- surfaceContainerLowest = Color(0xFF0D0E11),
- surfaceContainerLow = Color(0xFF1A1C1E),
- surfaceContainer = Color(0xFF1E2023),
- surfaceContainerHigh = Color(0xFF282A2D),
- surfaceContainerHighest = Color(0xFF333538)
-)
-
-/**
- * 应用主题
- * @param [darkTheme] 黑暗主题
- * @param [content] 内容
- */
-@Composable
-fun AppTheme(
- darkTheme: Boolean = isSystemInDarkTheme(),
- content: @Composable () -> Unit,
-) {
- val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
-
- MaterialTheme(
- colorScheme = colorScheme,
- typography = mainTypography(),
- content = content
- )
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/theme/Type.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/theme/Type.kt
deleted file mode 100644
index 1ddadb0..0000000
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/common/theme/Type.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.taskttl.ui.theme
-
-import androidx.compose.material3.Typography
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.sp
-
-
-/**
- * 主要排版
- * @return [Typography]
- */
-@Composable
-fun mainTypography(): Typography {
- return Typography(
- bodyLarge = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 16.sp,
- lineHeight = 24.sp,
- letterSpacing = 0.5.sp
- ),
- titleLarge = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 22.sp,
- lineHeight = 28.sp,
- letterSpacing = 0.sp
- ),
- labelSmall = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Medium,
- fontSize = 11.sp,
- lineHeight = 16.sp,
- letterSpacing = 0.5.sp
- )
- )
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/AuthViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/AuthViewModel.kt
deleted file mode 100644
index 23b487b..0000000
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/AuthViewModel.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-package com.taskttl.presentation.features.auth
-
-import androidx.compose.runtime.Stable
-import androidx.lifecycle.viewModelScope
-import com.taskttl.core.base.BaseViewModel
-import com.taskttl.core.utils.LogUtils
-import com.taskttl.data.source.remote.api.TaskTTLApi
-import com.taskttl.data.source.remote.dto.response.AuthResult
-import com.taskttl.domain.repository.AuthRepository
-import kotlinx.coroutines.launch
-
-/**
- * 身份验证视图模型
- * @author admin
- * @date 2025/10/26
- * @constructor 创建[AuthViewModel]
- */
-@Stable
-class AuthViewModel(private val authRepository: AuthRepository) :
- BaseViewModel(AuthState()) {
-
-
- public override fun handleIntent(intent: AuthIntent) {
- when (intent) {
- is AuthIntent.LoginWithGoogle -> onLoginWithGoogle()
-
- is AuthIntent.LoginWithFacebook -> onLoginWithFacebook()
-
- is AuthIntent.ClearError -> clearError()
- is AuthIntent.Logout -> {}
- }
- }
-
- private fun setLoading(loading: Boolean) {
- updateState { copy(isLoading = loading, isProcessing = false, error = null) }
- }
-
- /**
- * 清除错误
- */
- private fun clearError() {
- updateState { copy(error = null) }
- }
-
- /**
- * 使用谷歌登录
- */
- private fun onLoginWithGoogle() {
- LogUtils.e("DevTTL", "Google 登录触发")
- viewModelScope.launch {
- when (val result = authRepository.loginWithGoogle()) {
- is AuthResult.Success -> {
- if (result.account.isEmpty) {
- updateState { copy(isLoading = false, error = "Login canceled") }
- return@launch
- }
-
- TaskTTLApi.thirdPartyLogin(result.account)
- updateState { copy(isLoading = false, error = null) }
- // sendEvent(AuthEffect.NavigateToHome)
- }
-
- is AuthResult.Canceled -> {
- updateState { copy(isLoading = false, error = "Login canceled") }
- sendEvent(AuthEffect.ShowMessage("Login canceled"))
- }
-
- is AuthResult.Error -> {
- updateState {
- copy(
- isLoading = false,
- error = result.message ?: "Unknown error"
- )
- }
- sendEvent(AuthEffect.ShowMessage(result.message ?: "Unknown error"))
- }
- }
- }
- }
-
- /**
- * 使用脸书登录
- */
- private fun onLoginWithFacebook() {
- LogUtils.e("DevTTL", "Facebook 登录触发")
- viewModelScope.launch {
- val result = authRepository.loginWithFacebook()
- }
- }
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryScreen.kt
index 9fec21a..acacad7 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryScreen.kt
@@ -53,6 +53,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
+import com.taskttl.core.ui.component.empty.Empty
import com.taskttl.core.utils.ToastUtils
import com.taskttl.domain.model.Category
@@ -70,6 +71,8 @@ import taskttl.composeapp.generated.resources.label_countdown_count
import taskttl.composeapp.generated.resources.edit
import taskttl.composeapp.generated.resources.label_no_category
import taskttl.composeapp.generated.resources.label_task_count
+import taskttl.composeapp.generated.resources.text_add_task_hint
+import taskttl.composeapp.generated.resources.text_no_tasks
import taskttl.composeapp.generated.resources.title_add_category
import taskttl.composeapp.generated.resources.title_category
@@ -93,8 +96,6 @@ fun CategoryScreen(
is CategoryEffect.NavigateBack -> {
onNavigateBack.invoke()
}
-
- else -> {}
}
}
}
@@ -150,23 +151,10 @@ fun CategoryScreen(
CircularProgressIndicator()
}
} else if (categories.isEmpty()) {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
- Column(horizontalAlignment = Alignment.CenterHorizontally) {
- Text(
- text = stringResource(Res.string.label_no_category),
- style = MaterialTheme.typography.bodyLarge
- )
- Spacer(modifier = Modifier.height(8.dp))
- Text(
- text = stringResource(Res.string.label_add_category_hint),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- }
+ Empty(
+ message = Res.string.label_no_category,
+ subtitle = Res.string.label_add_category_hint
+ )
} else {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryState.kt
index 20ec3ad..2183fc7 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryState.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/category/list/CategoryState.kt
@@ -87,8 +87,19 @@ sealed class CategoryIntent {
* @constructor 创建[CategoryEffect]
*/
sealed class CategoryEffect {
- data class ShowMessage(val message: String) : CategoryEffect()
- data class NavigateToCategoryDetail(val categoryId: String) : CategoryEffect()
+ /**
+ * 导航返回
+ * @author admin
+ * @date 2025/11/26
+ */
object NavigateBack : CategoryEffect()
- data class ShowConfirmDialog(val message: String, val onConfirm: () -> Unit) : CategoryEffect()
+
+ /**
+ * 显示消息
+ * @author admin
+ * @date 2025/11/26
+ * @constructor 创建[ShowMessage]
+ * @param [message] 消息
+ */
+ data class ShowMessage(val message: String) : CategoryEffect()
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownScreen.kt
index 2c1a5f8..5c5e518 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownScreen.kt
@@ -55,6 +55,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
+import com.taskttl.core.ui.component.empty.Empty
import com.taskttl.core.utils.DateUtils
import com.taskttl.core.utils.ToastUtils
import com.taskttl.domain.model.Countdown
@@ -150,23 +151,10 @@ fun CountdownScreen(
}
state.filteredCountdowns.isEmpty() -> {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
- Column(horizontalAlignment = Alignment.CenterHorizontally) {
- Text(
- text = stringResource(Res.string.text_no_countdowns),
- style = MaterialTheme.typography.bodyLarge
- )
- Spacer(modifier = Modifier.height(8.dp))
- Text(
- text = stringResource(Res.string.text_add_countdown_tip),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- }
+ Empty(
+ message = Res.string.text_no_countdowns,
+ subtitle = Res.string.text_add_countdown_tip
+ )
}
else -> {
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownState.kt
index 89d0c64..f22583b 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownState.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/countdown/list/CountdownState.kt
@@ -52,6 +52,20 @@ sealed class CountdownIntent {
* @constructor 创建[CountdownEffect]
*/
sealed class CountdownEffect {
- data class ShowMessage(val message: String) : CountdownEffect()
+ /**
+ * 导航返回
+ * @author admin
+ * @date 2025/11/26
+ */
object NavigateBack : CountdownEffect()
+
+ /**
+ * 显示消息
+ * @author admin
+ * @date 2025/11/26
+ * @constructor 创建[ShowMessage]
+ * @param [message] 消息
+ */
+ data class ShowMessage(val message: String) : CountdownEffect()
+
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingScreen.kt
index 5a00b2e..bb43703 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingScreen.kt
@@ -1,5 +1,5 @@
package com.taskttl.presentation.features.onboarding
-
+// 引导视图
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingState.kt
index ae44f08..5cc6ada 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingState.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingState.kt
@@ -1,5 +1,5 @@
package com.taskttl.presentation.features.onboarding
-
+// 引导状态
import com.taskttl.core.base.BaseState
import com.taskttl.domain.model.OnboardingPage
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingViewModel.kt
index 1ca4a49..f82f963 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/onboarding/OnboardingViewModel.kt
@@ -14,7 +14,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
- * 入职视图模型
+ * 引导视图模型
* @author admin
* @date 2025/10/05
* @constructor 创建[OnboardingViewModel]
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/about/AboutScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/about/AboutScreen.kt
index 39a3895..81e0fe5 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/about/AboutScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/about/AboutScreen.kt
@@ -1,5 +1,5 @@
package com.taskttl.presentation.features.settings.about
-
+// 关于屏幕
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -32,6 +32,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import com.taskttl.core.ui.component.divider.Divider
import com.taskttl.presentation.common.components.AppHeader
import com.taskttl.presentation.features.settings.main.SettingsIntent
import com.taskttl.presentation.features.settings.main.SettingsViewModel
@@ -59,6 +60,11 @@ import taskttl.composeapp.generated.resources.version
import taskttl.composeapp.generated.resources.web_text
import taskttl.composeapp.generated.resources.web_url
+/**
+ * 关于屏幕
+ * @param [onNavigateBack] 上导航返回
+ * @param [viewModel] 视图模型
+ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AboutScreen(
@@ -131,6 +137,9 @@ fun AboutScreen(
)
}
}
+
+ Divider()
+
Spacer(modifier = Modifier.height(16.dp))
// 应用描述
Card(
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementScreen.kt
index e8839d5..54d067d 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementScreen.kt
@@ -1,86 +1,78 @@
package com.taskttl.presentation.features.settings.dataManagement
-
+// 数据管理屏幕
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.AttachFile
-import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.CleaningServices
-import androidx.compose.material.icons.filled.CloudDownload
-import androidx.compose.material.icons.filled.CloudUpload
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.History
-import androidx.compose.material.icons.filled.Sync
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedButton
-import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
+import com.taskttl.core.utils.ToastUtils
import com.taskttl.presentation.common.components.AppHeader
-import org.jetbrains.compose.resources.StringResource
+import com.taskttl.presentation.features.settings.dataManagement.components.ConfirmDialog
+import com.taskttl.presentation.features.settings.dataManagement.components.DataManagementCard
+import com.taskttl.presentation.features.settings.dataManagement.components.ExportDataDialog
+import com.taskttl.presentation.features.settings.dataManagement.components.ImportDataDialog
import org.jetbrains.compose.resources.stringResource
+import org.koin.compose.viewmodel.koinViewModel
import taskttl.composeapp.generated.resources.Res
-import taskttl.composeapp.generated.resources.cancel
-import taskttl.composeapp.generated.resources.confirm
-import taskttl.composeapp.generated.resources.desc_auto_backup
import taskttl.composeapp.generated.resources.desc_clear_all_data
import taskttl.composeapp.generated.resources.desc_clear_all_data_dialog
import taskttl.composeapp.generated.resources.desc_clear_completed_tasks
import taskttl.composeapp.generated.resources.desc_clear_expired_countdowns
-import taskttl.composeapp.generated.resources.desc_export_data
-import taskttl.composeapp.generated.resources.desc_import_data
-import taskttl.composeapp.generated.resources.export
-import taskttl.composeapp.generated.resources.import
-import taskttl.composeapp.generated.resources.label_csv_format
-import taskttl.composeapp.generated.resources.enter
-import taskttl.composeapp.generated.resources.label_json_format
-import taskttl.composeapp.generated.resources.label_select_export_format
-import taskttl.composeapp.generated.resources.label_select_file
-import taskttl.composeapp.generated.resources.label_select_import_file
-import taskttl.composeapp.generated.resources.title_auto_backup
-import taskttl.composeapp.generated.resources.title_backup_restore
import taskttl.composeapp.generated.resources.title_clear_all_data
import taskttl.composeapp.generated.resources.title_clear_completed_tasks
import taskttl.composeapp.generated.resources.title_clear_expired_countdowns
import taskttl.composeapp.generated.resources.title_data_clean
import taskttl.composeapp.generated.resources.title_data_management
-import taskttl.composeapp.generated.resources.title_export_data
-import taskttl.composeapp.generated.resources.title_import_data
+/**
+ * 数据管理屏幕
+ * @param [onNavigateBack] 上导航返回
+ * @param [viewModel] 视图模型
+ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DataManagementScreen(
- onNavigateBack: () -> Unit
+ onNavigateBack: () -> Unit,
+ viewModel: DataManagementViewModel = koinViewModel()
) {
+ val state by viewModel.state.collectAsState()
+
+ LaunchedEffect(Unit) {
+ viewModel.effects.collect { effect ->
+ when (effect) {
+ is DataManagementEffect.NavigateBack -> onNavigateBack.invoke()
+ is DataManagementEffect.ShowMessage -> ToastUtils.show(effect.message)
+ }
+ }
+ }
+
var showExportDialog by remember { mutableStateOf(false) }
var showImportDialog by remember { mutableStateOf(false) }
+
var showClearDataDialog by remember { mutableStateOf(false) }
+ var showClearCompletedTasksDialog by remember { mutableStateOf(false) }
+ var showClearExpiredCountdownsDialog by remember { mutableStateOf(false) }
Box(
modifier = Modifier
@@ -103,39 +95,39 @@ fun DataManagementScreen(
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
- item {
- Text(
- text = stringResource(Res.string.title_backup_restore),
- style = MaterialTheme.typography.titleLarge
- )
- }
-
- item {
- DataManagementCard(
- icon = Icons.Default.CloudUpload,
- titleRes = Res.string.title_export_data,
- descriptionRes = Res.string.desc_export_data,
- onClick = { showExportDialog = true }
- )
- }
-
- item {
- DataManagementCard(
- icon = Icons.Default.CloudDownload,
- titleRes = Res.string.title_import_data,
- descriptionRes = Res.string.desc_import_data,
- onClick = { showImportDialog = true }
- )
- }
-
- item {
- DataManagementCard(
- icon = Icons.Default.Sync,
- titleRes = Res.string.title_auto_backup,
- descriptionRes = Res.string.desc_auto_backup,
- onClick = { /* TODO: 自动备份设置 */ }
- )
- }
+ // item {
+ // Text(
+ // text = stringResource(Res.string.title_backup_restore),
+ // style = MaterialTheme.typography.titleLarge
+ // )
+ // }
+ //
+ // item {
+ // DataManagementCard(
+ // icon = Icons.Default.CloudUpload,
+ // titleRes = Res.string.title_export_data,
+ // descriptionRes = Res.string.desc_export_data,
+ // onClick = { showExportDialog = true }
+ // )
+ // }
+ //
+ // item {
+ // DataManagementCard(
+ // icon = Icons.Default.CloudDownload,
+ // titleRes = Res.string.title_import_data,
+ // descriptionRes = Res.string.desc_import_data,
+ // onClick = { showImportDialog = true }
+ // )
+ // }
+ //
+ // item {
+ // DataManagementCard(
+ // icon = Icons.Default.Sync,
+ // titleRes = Res.string.title_auto_backup,
+ // descriptionRes = Res.string.desc_auto_backup,
+ // onClick = { /* TODO: 自动备份设置 */ }
+ // )
+ // }
item {
Spacer(modifier = Modifier.height(16.dp))
@@ -145,6 +137,7 @@ fun DataManagementScreen(
)
}
+ // 清除全部数据
item {
DataManagementCard(
icon = Icons.Default.Delete,
@@ -155,21 +148,23 @@ fun DataManagementScreen(
)
}
+ // 清理已完成任务
item {
DataManagementCard(
icon = Icons.Default.CleaningServices,
titleRes = Res.string.title_clear_completed_tasks,
descriptionRes = Res.string.desc_clear_completed_tasks,
- onClick = { /* TODO: 清理已完成任务 */ }
+ onClick = { showClearCompletedTasksDialog = true }
)
}
+ // 清理过期倒数日
item {
DataManagementCard(
icon = Icons.Default.History,
titleRes = Res.string.title_clear_expired_countdowns,
descriptionRes = Res.string.desc_clear_expired_countdowns,
- onClick = { /* TODO: 清理过期倒数日 */ }
+ onClick = { showClearExpiredCountdownsDialog = true }
)
}
}
@@ -198,173 +193,35 @@ fun DataManagementScreen(
}
// 清除数据确认对话框
- if (showClearDataDialog) {
- AlertDialog(
- onDismissRequest = { showClearDataDialog = false },
- title = { Text(stringResource(Res.string.title_clear_all_data)) },
- text = { Text(stringResource(Res.string.desc_clear_all_data_dialog)) },
- confirmButton = {
- TextButton(
- onClick = {
- // TODO: 实现清除数据功能
- showClearDataDialog = false
- }
- ) {
- Text(stringResource(Res.string.confirm), color = MaterialTheme.colorScheme.error)
- }
- },
- dismissButton = {
- TextButton(onClick = { showClearDataDialog = false }) {
- Text(stringResource(Res.string.cancel))
- }
- }
- )
- }
+ ConfirmDialog(
+ visible = showClearDataDialog,
+ title = stringResource(Res.string.title_clear_all_data),
+ message = stringResource(Res.string.desc_clear_all_data_dialog),
+ confirmIsDestructive = true,
+ onConfirm = { viewModel.processIntent(DataManagementIntent.ClearAllData) },
+ onDismiss = { showClearDataDialog = false }
+ )
+
+
+ // 清理已完成任务确认对话框
+ ConfirmDialog(
+ visible = showClearCompletedTasksDialog,
+ title = stringResource(Res.string.title_clear_completed_tasks),
+ message = stringResource(Res.string.desc_clear_completed_tasks),
+ confirmIsDestructive = true,
+ onConfirm = { viewModel.processIntent(DataManagementIntent.ClearCompletedTasks) },
+ onDismiss = { showClearCompletedTasksDialog = false }
+ )
+
+
+ // 清理过期倒数日确认对话框
+ ConfirmDialog(
+ visible = showClearExpiredCountdownsDialog,
+ title = stringResource(Res.string.title_clear_expired_countdowns),
+ message = stringResource(Res.string.desc_clear_expired_countdowns),
+ confirmIsDestructive = true,
+ onConfirm = { viewModel.processIntent(DataManagementIntent.ClearExpiredCountdowns) },
+ onDismiss = { showClearExpiredCountdownsDialog = false }
+ )
}
}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun DataManagementCard(
- icon: ImageVector,
- titleRes: StringResource,
- descriptionRes: StringResource,
- onClick: () -> Unit,
- isDestructive: Boolean = false
-) {
- Card(
- onClick = onClick,
- colors = if (isDestructive) {
- CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f)
- )
- } else {
- CardDefaults.cardColors()
- }
- ) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- imageVector = icon,
- contentDescription = stringResource(titleRes),
- tint = if (isDestructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
- modifier = Modifier.size(32.dp)
- )
-
- Spacer(modifier = Modifier.width(16.dp))
-
- Column(
- modifier = Modifier.weight(1f)
- ) {
- Text(
- text = stringResource(titleRes),
- style = MaterialTheme.typography.titleMedium,
- color = if (isDestructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface
- )
- Text(
- text = stringResource(descriptionRes),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
-
- Icon(
- imageVector = Icons.Default.ChevronRight,
- contentDescription = stringResource(Res.string.enter),
- tint = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- }
-}
-
-@Composable
-private fun ExportDataDialog(
- onDismiss: () -> Unit,
- onExport: (String) -> Unit
-) {
- var selectedFormat by remember { mutableStateOf("JSON") }
-
- AlertDialog(
- onDismissRequest = onDismiss,
- title = { Text(stringResource(Res.string.title_export_data)) },
- text = {
- Column {
- Text("${stringResource(Res.string.label_select_export_format)}:")
- Spacer(modifier = Modifier.height(16.dp))
-
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- RadioButton(
- selected = selectedFormat == "JSON",
- onClick = { selectedFormat = "JSON" }
- )
- Text(stringResource(Res.string.label_json_format))
- }
-
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- RadioButton(
- selected = selectedFormat == "CSV",
- onClick = { selectedFormat = "CSV" }
- )
- Text(stringResource(Res.string.label_csv_format))
- }
- }
- },
- confirmButton = {
- TextButton(onClick = { onExport(selectedFormat) }) {
- Text(stringResource(Res.string.export))
- }
- },
- dismissButton = {
- TextButton(onClick = onDismiss) {
- Text(stringResource(Res.string.cancel))
- }
- }
- )
-}
-
-@Composable
-private fun ImportDataDialog(
- onDismiss: () -> Unit,
- onImport: () -> Unit
-) {
- AlertDialog(
- onDismissRequest = onDismiss,
- title = { Text(stringResource(Res.string.title_import_data)) },
- text = {
- Column {
- Text("${stringResource(Res.string.label_select_import_file)}:")
- Spacer(modifier = Modifier.height(16.dp))
-
- OutlinedButton(
- onClick = { /* TODO: 文件选择器 */ },
- modifier = Modifier.fillMaxWidth()
- ) {
- Icon(
- Icons.Default.AttachFile,
- contentDescription = stringResource(Res.string.label_select_file)
- )
- Spacer(modifier = Modifier.width(8.dp))
- Text(stringResource(Res.string.label_select_file))
- }
- }
- },
- confirmButton = {
- TextButton(onClick = onImport) {
- Text(stringResource(Res.string.import))
- }
- },
- dismissButton = {
- TextButton(onClick = onDismiss) {
- Text(stringResource(Res.string.cancel))
- }
- }
- )
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementState.kt
new file mode 100644
index 0000000..72f39c2
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementState.kt
@@ -0,0 +1,61 @@
+package com.taskttl.presentation.features.settings.dataManagement
+// 数据管理状态
+import com.taskttl.core.base.BaseState
+
+/**
+ * 数据管理状态
+ * @author admin
+ * @date 2025/11/26
+ * @constructor 创建[DataManagementState]
+ * @param [isLoading] 正在加载
+ * @param [isProcessing] 正在处理
+ * @param [error] 错误
+ */
+data class DataManagementState(
+ override val isLoading: Boolean = false,
+ override val isProcessing: Boolean = false,
+ override val error: String? = null,
+) : BaseState()
+
+/**
+ * 数据管理意图
+ * @author admin
+ * @date 2025/11/26
+ * @constructor 创建[DataManagementIntent]
+ */
+sealed class DataManagementIntent {
+ /** 清除所有数据 */
+ data object ClearAllData : DataManagementIntent()
+
+ /** 清理已完成任务 */
+ data object ClearCompletedTasks : DataManagementIntent()
+
+ /** 清理过期倒数日 */
+ data object ClearExpiredCountdowns : DataManagementIntent()
+}
+
+
+/**
+ * 数据管理效果
+ * @author admin
+ * @date 2025/11/26
+ * @constructor 创建[DataManagementEffect]
+ */
+sealed class DataManagementEffect {
+
+ /**
+ * 导航返回
+ * @author admin
+ * @date 2025/10/12
+ */
+ object NavigateBack : DataManagementEffect()
+
+ /**
+ * 显示消息
+ * @author admin
+ * @date 2025/10/12
+ * @constructor 创建[ShowMessage]
+ * @param [message] 消息
+ */
+ data class ShowMessage(val message: String) : DataManagementEffect()
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementViewModel.kt
new file mode 100644
index 0000000..1202c4c
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/DataManagementViewModel.kt
@@ -0,0 +1,62 @@
+package com.taskttl.presentation.features.settings.dataManagement
+
+import androidx.compose.runtime.Stable
+import androidx.lifecycle.viewModelScope
+import com.taskttl.core.base.BaseViewModel
+import com.taskttl.domain.repository.CountdownRepository
+import com.taskttl.domain.repository.TaskRepository
+import kotlinx.coroutines.launch
+
+/**
+ * 数据管理视图模型
+ * @author admin
+ * @date 2025/11/26
+ * @constructor 创建[DataManagementViewModel]
+ */
+@Stable
+class DataManagementViewModel(
+ private val taskRepository: TaskRepository,
+ private val countdownRepository: CountdownRepository
+) : BaseViewModel(DataManagementState()) {
+
+
+ override fun handleIntent(intent: DataManagementIntent) {
+ when (intent) {
+ is DataManagementIntent.ClearAllData -> clearAllData()
+ is DataManagementIntent.ClearCompletedTasks -> clearCompletedTasks()
+ is DataManagementIntent.ClearExpiredCountdowns -> clearExpiredCountdowns()
+ }
+ }
+
+ /**
+ * 清除所有数据
+ */
+ private fun clearAllData() {
+ viewModelScope.launch {
+ taskRepository.deleteAllTasks()
+ countdownRepository.deleteAllCountdowns()
+ sendEvent(DataManagementEffect.ShowMessage("已清除所有数据"))
+ }
+ }
+
+ /**
+ * 清除已完成任务
+ */
+ private fun clearCompletedTasks() {
+ viewModelScope.launch {
+ taskRepository.deleteCompletedTasks()
+ sendEvent(DataManagementEffect.ShowMessage("已清除已完成任务"))
+ }
+ }
+
+ /**
+ * 清除过期倒计时
+ */
+ private fun clearExpiredCountdowns() {
+ viewModelScope.launch {
+ countdownRepository.clearExpiredCountdowns()
+ sendEvent(DataManagementEffect.ShowMessage("已清除过期倒计时"))
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ConfirmDialog.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ConfirmDialog.kt
new file mode 100644
index 0000000..2c130b9
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ConfirmDialog.kt
@@ -0,0 +1,53 @@
+package com.taskttl.presentation.features.settings.dataManagement.components
+// 确认对话框
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import org.jetbrains.compose.resources.stringResource
+import taskttl.composeapp.generated.resources.Res
+import taskttl.composeapp.generated.resources.cancel
+import taskttl.composeapp.generated.resources.confirm
+
+/**
+ * 确认对话框
+ * @param [visible] 可见
+ * @param [title] 标题
+ * @param [message] 消息
+ * @param [confirmText] 确认文本
+ * @param [cancelText] 取消文本
+ * @param [confirmIsDestructive] 确认具有破坏性
+ * @param [onConfirm] 确认
+ * @param [onDismiss] 解雇
+ */
+@Composable
+fun ConfirmDialog(
+ visible: Boolean,
+ title: String,
+ message: String,
+ confirmText: String = stringResource(Res.string.confirm),
+ cancelText: String = stringResource(Res.string.cancel),
+ confirmIsDestructive: Boolean = false,
+ onConfirm: () -> Unit,
+ onDismiss: () -> Unit
+) {
+ if (!visible) return
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text(title) },
+ text = { Text(message) },
+ confirmButton = {
+ TextButton(onClick = {
+ onConfirm()
+ onDismiss()
+ }) {
+ Text(
+ confirmText,
+ color = if (confirmIsDestructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary
+ )
+ }
+ },
+ dismissButton = { TextButton(onClick = onDismiss) { Text(cancelText) } }
+ )
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/DataManagementCard.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/DataManagementCard.kt
new file mode 100644
index 0000000..a4e1980
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/DataManagementCard.kt
@@ -0,0 +1,92 @@
+package com.taskttl.presentation.features.settings.dataManagement.components
+// 数据管理卡片
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ChevronRight
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.dp
+import org.jetbrains.compose.resources.StringResource
+import org.jetbrains.compose.resources.stringResource
+import taskttl.composeapp.generated.resources.Res
+import taskttl.composeapp.generated.resources.enter
+
+/**
+ * 数据管理卡
+ * @param [icon] 图标
+ * @param [titleRes] 标题res
+ * @param [descriptionRes] 描述res
+ * @param [onClick] 点击时
+ * @param [isDestructive] 具有破坏性
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DataManagementCard(
+ icon: ImageVector,
+ titleRes: StringResource,
+ descriptionRes: StringResource,
+ onClick: () -> Unit,
+ isDestructive: Boolean = false
+) {
+ Card(
+ onClick = onClick,
+ colors = if (isDestructive) {
+ CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f)
+ )
+ } else {
+ CardDefaults.cardColors()
+ }
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = stringResource(titleRes),
+ tint = if (isDestructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(32.dp)
+ )
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
+ Text(
+ text = stringResource(titleRes),
+ style = MaterialTheme.typography.titleMedium,
+ color = if (isDestructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface
+ )
+ Text(
+ text = stringResource(descriptionRes),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ Icon(
+ imageVector = Icons.Default.ChevronRight,
+ contentDescription = stringResource(Res.string.enter),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ExportDataDialog.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ExportDataDialog.kt
new file mode 100644
index 0000000..716cc47
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ExportDataDialog.kt
@@ -0,0 +1,81 @@
+package com.taskttl.presentation.features.settings.dataManagement.components
+// 导出数据对话框
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import org.jetbrains.compose.resources.stringResource
+import taskttl.composeapp.generated.resources.Res
+import taskttl.composeapp.generated.resources.cancel
+import taskttl.composeapp.generated.resources.export
+import taskttl.composeapp.generated.resources.label_csv_format
+import taskttl.composeapp.generated.resources.label_json_format
+import taskttl.composeapp.generated.resources.label_select_export_format
+import taskttl.composeapp.generated.resources.title_export_data
+
+
+/**
+ * 导出数据对话框
+ * @param [onDismiss] 解雇
+ * @param [onExport] 关于出口
+ */
+@Composable
+fun ExportDataDialog(
+ onDismiss: () -> Unit,
+ onExport: (String) -> Unit
+) {
+ var selectedFormat by remember { mutableStateOf("JSON") }
+
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text(stringResource(Res.string.title_export_data)) },
+ text = {
+ Column {
+ Text("${stringResource(Res.string.label_select_export_format)}:")
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(
+ selected = selectedFormat == "JSON",
+ onClick = { selectedFormat = "JSON" }
+ )
+ Text(stringResource(Res.string.label_json_format))
+ }
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(
+ selected = selectedFormat == "CSV",
+ onClick = { selectedFormat = "CSV" }
+ )
+ Text(stringResource(Res.string.label_csv_format))
+ }
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = { onExport(selectedFormat) }) {
+ Text(stringResource(Res.string.export))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text(stringResource(Res.string.cancel))
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ImportDataDialog.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ImportDataDialog.kt
new file mode 100644
index 0000000..c83a97e
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/dataManagement/components/ImportDataDialog.kt
@@ -0,0 +1,68 @@
+package com.taskttl.presentation.features.settings.dataManagement.components
+// 导入数据对话框
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AttachFile
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import org.jetbrains.compose.resources.stringResource
+import taskttl.composeapp.generated.resources.Res
+import taskttl.composeapp.generated.resources.cancel
+import taskttl.composeapp.generated.resources.import
+import taskttl.composeapp.generated.resources.label_select_file
+import taskttl.composeapp.generated.resources.label_select_import_file
+import taskttl.composeapp.generated.resources.title_import_data
+
+/**
+ * 导入数据对话框
+ * @param [onDismiss] 解雇
+ * @param [onImport] 关于导入
+ */
+@Composable
+fun ImportDataDialog(
+ onDismiss: () -> Unit,
+ onImport: () -> Unit
+) {
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text(stringResource(Res.string.title_import_data)) },
+ text = {
+ Column {
+ Text("${stringResource(Res.string.label_select_import_file)}:")
+ Spacer(modifier = Modifier.height(16.dp))
+
+ OutlinedButton(
+ onClick = { /* TODO: 文件选择器 */ },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Icon(
+ Icons.Default.AttachFile,
+ contentDescription = stringResource(Res.string.label_select_file)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(stringResource(Res.string.label_select_file))
+ }
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = onImport) {
+ Text(stringResource(Res.string.import))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text(stringResource(Res.string.cancel))
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackScreen.kt
index 0c59c0c..806df02 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackScreen.kt
@@ -1,5 +1,5 @@
package com.taskttl.presentation.features.settings.feedback
-
+// 反馈屏幕
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@@ -59,6 +59,11 @@ import taskttl.composeapp.generated.resources.feedback_placeholder
import taskttl.composeapp.generated.resources.feedback_type
import taskttl.composeapp.generated.resources.title_feedback
+/**
+ * 反馈屏幕
+ * @param [onNavigateBack] 上导航返回
+ * @param [viewModel] 视图模型
+ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FeedbackScreen(
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackState.kt
index eb191a8..d6a5a2f 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackState.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/feedback/FeedbackState.kt
@@ -1,8 +1,17 @@
package com.taskttl.presentation.features.settings.feedback
-
+// 反馈状态
import com.taskttl.core.base.BaseState
import com.taskttl.data.source.remote.dto.request.FeedbackReq
+/**
+ * 反馈状态
+ * @author admin
+ * @date 2025/11/26
+ * @constructor 创建[FeedbackState]
+ * @param [isLoading] 正在加载
+ * @param [isProcessing] 正在处理
+ * @param [error] 错误
+ */
data class FeedbackState(
override val isLoading: Boolean = false,
override val isProcessing: Boolean = false,
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsScreen.kt
index d5c6fb4..185f23e 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsScreen.kt
@@ -1,5 +1,5 @@
package com.taskttl.presentation.features.settings.main
-
+// 设置屏幕
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -38,13 +37,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
+import com.taskttl.core.auth.AuthManager
import com.taskttl.core.manager.ThemeMode
import com.taskttl.core.permission.NotificationPermissionManager
import com.taskttl.navigation.Routes
@@ -80,6 +78,7 @@ import taskttl.composeapp.generated.resources.title_app_settings
/**
* 设置屏幕
* @param [navController] 导航控制器
+ * @param [viewModel] 视图模型
*/
@Composable
fun SettingsScreen(
@@ -123,22 +122,27 @@ fun SettingsScreen(
) {
// 用户信息卡片(登录前/后通用组件)
UserInfoCard(
- isLoggedIn = true,
- userName = "DevTTL",
- userSubtitle = "111111",
- onClick = { navController.navigate(Routes.Main.Settings.Login) }
+ isLoggedIn = !AuthManager.isExpired(),
+ userInfo = state.userInfo,
+ onClick = {
+ if (AuthManager.isExpired()) {
+ navController.navigate(Routes.User.Login)
+ } else {
+ navController.navigate(Routes.User.UserProfile)
+ }
+ }
)
- Spacer(modifier = Modifier.height(12.dp))
-
- Card(
- modifier = Modifier.fillMaxWidth().height(80.dp),
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.surface
- )
- ) {
-
- }
+ // Spacer(modifier = Modifier.height(12.dp))
+ //
+ // Card(
+ // modifier = Modifier.fillMaxWidth().height(80.dp),
+ // colors = CardDefaults.cardColors(
+ // containerColor = MaterialTheme.colorScheme.surface
+ // )
+ // ) {
+ //
+ // }
Spacer(modifier = Modifier.height(24.dp))
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsState.kt
index f12efd6..463f15f 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsState.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsState.kt
@@ -1,8 +1,10 @@
package com.taskttl.presentation.features.settings.main
-
+// 设置状态
+import com.taskttl.core.auth.AuthManager
import com.taskttl.core.base.BaseState
import com.taskttl.core.manager.ThemeMode
import com.taskttl.core.permission.NotificationPermissionManager
+import com.taskttl.domain.model.UserInfo
/**
* 设置状态
@@ -16,6 +18,7 @@ data class SettingsState(
override val isLoading: Boolean = false,
override val isProcessing: Boolean = false,
override val error: String? = null,
+ val userInfo: UserInfo? = AuthManager.userInfo.value,
val themeMode: ThemeMode = ThemeMode.SYSTEM,
val showThemeDialog: Boolean = false,
val isNotification: Boolean = NotificationPermissionManager.verifyPermission(),
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsViewModel.kt
index 0353dd2..007014c 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/SettingsViewModel.kt
@@ -2,6 +2,7 @@ package com.taskttl.presentation.features.settings.main
import androidx.compose.runtime.Stable
import androidx.lifecycle.viewModelScope
+import com.taskttl.core.auth.AuthManager
import com.taskttl.core.base.BaseViewModel
import com.taskttl.core.manager.ThemeManager
import com.taskttl.core.manager.ThemeMode
@@ -11,10 +12,11 @@ import com.taskttl.core.utils.ExternalAppLauncher
import kotlinx.coroutines.launch
/**
- * 反馈视图模型
+ * 设置视图模型
* @author admin
- * @date 2025/10/12
+ * @date 2025/11/26
* @constructor 创建[SettingsViewModel]
+ * @param [themeManager] 主题管理器
*/
@Stable
class SettingsViewModel(private val themeManager: ThemeManager) :
@@ -25,6 +27,10 @@ class SettingsViewModel(private val themeManager: ThemeManager) :
viewModelScope.launch {
themeManager.themeMode.collect { mode -> updateState { copy(themeMode = mode) } }
}
+
+ viewModelScope.launch {
+ AuthManager.userInfo.collect { userInfo -> updateState { copy(userInfo = userInfo) } }
+ }
}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/common/UserInfoCard.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/common/UserInfoCard.kt
index 107ac02..e63bac1 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/common/UserInfoCard.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/main/common/UserInfoCard.kt
@@ -1,5 +1,5 @@
package com.taskttl.presentation.features.settings.main.common
-
+// 用户信息卡片
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -13,9 +13,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -24,27 +22,28 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import coil3.compose.AsyncImage
+import com.taskttl.domain.model.UserInfo
import com.taskttl.presentation.common.components.NetworkImage
-import taskttl.composeapp.generated.resources.Res
/**
* 用户信息卡片
* 支持:
* - 未登录:展示登录引导
* - 已登录:展示头像首字母、昵称、副标题
+ * 用户信息卡
+ * @param [isLoggedIn] 已登录
+ * @param [userInfo] 用户信息
+ * @param [onClick] 点击时
*/
@Composable
fun UserInfoCard(
isLoggedIn: Boolean,
- userName: String?,
- userSubtitle: String?,
+ userInfo: UserInfo?,
onClick: () -> Unit,
) {
@@ -61,15 +60,15 @@ fun UserInfoCard(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.SpaceBetween
) {
- if (isLoggedIn && !userName.isNullOrBlank()) {
+ if (isLoggedIn && userInfo != null) {
Text(
- text = userName,
+ text = userInfo.nickname,
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)
Text(
- text = userSubtitle ?: "欢迎回来,继续保持高效!",
+ text = userInfo.email,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 14.sp
)
@@ -88,14 +87,6 @@ fun UserInfoCard(
}
}
- if (!isLoggedIn) {
- Icon(
- imageVector = Icons.Default.ChevronRight,
- contentDescription = null,
- tint = Color.White.copy(alpha = 0.9f)
- )
- }
-
Spacer(modifier = Modifier.width(12.dp))
// 头像/占位
@@ -105,9 +96,9 @@ fun UserInfoCard(
.background(Color.White.copy(alpha = 0.2f), shape = CircleShape),
contentAlignment = Alignment.Center
) {
- if (isLoggedIn && !userName.isNullOrBlank()) {
+ if (isLoggedIn && userInfo != null) {
NetworkImage(
- url = Res.getUri("drawable/ic_launcher.png"),
+ url = userInfo.avatar,
contentDescription = null,
modifier = Modifier.size(60.dp),
contentScale = ContentScale.Fit
@@ -116,7 +107,7 @@ fun UserInfoCard(
Icon(
imageVector = Icons.Default.Person,
contentDescription = "用户头像",
- tint = Color.White,
+ tint = Color.Black,
modifier = Modifier.size(40.dp)
)
}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/privacy/PrivacyScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/privacy/PrivacyScreen.kt
index a96162e..26bc2a8 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/privacy/PrivacyScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/settings/privacy/PrivacyScreen.kt
@@ -1,5 +1,5 @@
package com.taskttl.presentation.features.settings.privacy
-
+// 隐私屏幕
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -14,6 +14,10 @@ import taskttl.composeapp.generated.resources.Res
import taskttl.composeapp.generated.resources.privacy_url
import taskttl.composeapp.generated.resources.title_privacy
+/**
+ * 隐私屏幕
+ * @param [onNavigateBack] 上导航返回
+ */
@Composable
fun PrivacyScreen(onNavigateBack: () -> Unit) {
Box(modifier = Modifier.fillMaxSize()) {
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashScreen.kt
index 495cd2a..5eee6fc 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashScreen.kt
@@ -1,5 +1,5 @@
package com.taskttl.presentation.features.splash
-
+// 启动视图
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
@@ -53,13 +53,8 @@ fun SplashScreen(
LaunchedEffect(Unit) {
viewModel.effects.collect { effect ->
when (effect) {
- is SplashEffect.NavigateToOnboarding -> {
- navigatorToRoute(Routes.Onboarding)
- }
-
- is SplashEffect.NavigateToMain -> {
- navigatorToRoute(Routes.Main)
- }
+ is SplashEffect.NavigateToOnboarding -> navigatorToRoute(Routes.Onboarding)
+ is SplashEffect.NavigateToMain -> navigatorToRoute(Routes.Main)
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashState.kt
index 0defe63..2e47835 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashState.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashState.kt
@@ -1,5 +1,5 @@
package com.taskttl.presentation.features.splash
-
+// 启动页状态
import com.taskttl.core.base.BaseState
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashViewModel.kt
index 9f2bffc..5376c55 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/splash/SplashViewModel.kt
@@ -44,9 +44,15 @@ class SplashViewModel(
*/
private fun loadingStatus() {
viewModelScope.launch {
- delay(200)
+ DeviceUtils.getUniqueId()
+ val firstResult = StorageUtils.getBoolean(PointEvent.FirstAppLaunch.eventName, false)
+ if (!firstResult) {
+ TaskTTLApi.postPoint(PointEvent.FirstAppLaunch)
+ StorageUtils.saveBoolean(PointEvent.FirstAppLaunch.eventName, true)
+ } else {
+ TaskTTLApi.postPoint(PointEvent.AppLaunch)
+ }
val hasLaunched = onboardingRepository.isLaunchedBefore()
- LogUtils.e("DevTTL",hasLaunched.toString())
withContext(Dispatchers.Main) {
if (hasLaunched) {
sendEvent(SplashEffect.NavigateToOnboarding)
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/StatisticsScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/StatisticsScreen.kt
index 171e48a..02e2449 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/StatisticsScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/StatisticsScreen.kt
@@ -1,8 +1,8 @@
package com.taskttl.presentation.features.statistics
+// 统计屏幕
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -10,24 +10,14 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Assignment
import androidx.compose.material.icons.automirrored.filled.TrendingUp
import androidx.compose.material.icons.filled.CalendarToday
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Schedule
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -36,21 +26,17 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
-import com.taskttl.domain.model.Category
import com.taskttl.navigation.Routes
import com.taskttl.presentation.common.components.AppHeader
-import com.taskttl.presentation.common.components.Chip
import com.taskttl.presentation.features.countdown.list.CountdownIntent
import com.taskttl.presentation.features.countdown.list.CountdownViewModel
+import com.taskttl.presentation.features.statistics.components.CategoryStatisticItem
+import com.taskttl.presentation.features.statistics.components.StatisticCard
import com.taskttl.presentation.features.task.list.TaskIntent
import com.taskttl.presentation.features.task.list.TaskViewModel
-import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
import taskttl.composeapp.generated.resources.Res
@@ -64,6 +50,12 @@ import taskttl.composeapp.generated.resources.setting_category_management
import taskttl.composeapp.generated.resources.title_statistics
import taskttl.composeapp.generated.resources.total_tasks
+/**
+ * 统计屏幕
+ * @param [navController] 导航控制器
+ * @param [taskViewModel] 任务视图模型
+ * @param [countdownViewModel] 倒计时视图模型
+ */
@Composable
fun StatisticsScreen(
navController: NavHostController,
@@ -209,142 +201,3 @@ fun StatisticsScreen(
}
}
}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun StatisticCard(
- titleRes: StringResource,
- value: String,
- icon: ImageVector,
- color: Color,
- modifier: Modifier = Modifier,
-) {
- Card(
- modifier = modifier,
- colors = CardDefaults.cardColors(containerColor = color.copy(alpha = 0.1f))
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Icon(
- imageVector = icon,
- contentDescription = stringResource(titleRes),
- tint = color,
- modifier = Modifier.size(32.dp)
- )
-
- Spacer(modifier = Modifier.height(8.dp))
-
- Text(
- text = value,
- style = MaterialTheme.typography.headlineMedium,
- fontWeight = FontWeight.Bold,
- color = color
- )
-
- Text(
- text = stringResource(titleRes),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- }
-}
-
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun CategoryStatisticItem(
- category: Category,
- totalCount: Int,
- completedCount: Int,
- typeRes: StringResource,
-) {
- if (totalCount == 0) return
-
- Card(
- modifier = Modifier.fillMaxWidth(),
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
- ) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- // 分类颜色指示器
- Box(
- modifier = Modifier
- .size(40.dp)
- .clip(CircleShape)
- .background(category.color.backgroundColor),
- contentAlignment = Alignment.Center
- ) {
- Icon(
- imageVector = category.icon.icon,
- contentDescription = stringResource(category.icon.displayNameRes),
- tint = category.color.iconColor,
- modifier = Modifier.size(24.dp)
- )
- }
-
- Spacer(modifier = Modifier.width(16.dp))
-
- // 分类信息
- Column(
- modifier = Modifier.weight(1f)
- ) {
- Row() {
- Text(
- text = category.name,
- style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.Medium,
- color = category.color.textColor
- )
- Spacer(modifier = Modifier.width(6.dp))
-
- Chip(
- textRes = category.type.displayNameRes,
- )
- }
-
- Text(
- text = "${stringResource(typeRes)}: $completedCount/$totalCount",
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
-
- // 进度条
- Column(
- horizontalAlignment = Alignment.End
- ) {
- val progress = if (totalCount > 0) completedCount.toFloat() / totalCount else 0f
-
- Text(
- text = "${(progress * 100).toInt()}%",
- style = MaterialTheme.typography.labelMedium,
- fontWeight = FontWeight.Medium,
- color = category.color.textColor
- )
-
- Spacer(modifier = Modifier.height(4.dp))
-
- LinearProgressIndicator(
- progress = { progress },
- modifier = Modifier
- .width(80.dp)
- .height(6.dp)
- .clip(RoundedCornerShape(3.dp)),
- color = category.color.textColor,
- trackColor = ProgressIndicatorDefaults.linearTrackColor,
- strokeCap = ProgressIndicatorDefaults.LinearStrokeCap,
- )
- }
- }
- }
- Spacer(modifier = Modifier.height(10.dp))
-}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/components/CategoryStatisticItem.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/components/CategoryStatisticItem.kt
new file mode 100644
index 0000000..b179de8
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/components/CategoryStatisticItem.kt
@@ -0,0 +1,134 @@
+package com.taskttl.presentation.features.statistics.components
+// 类别统计项
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ProgressIndicatorDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.taskttl.domain.model.Category
+import com.taskttl.presentation.common.components.Chip
+import org.jetbrains.compose.resources.StringResource
+import org.jetbrains.compose.resources.stringResource
+
+
+/**
+ * 类别统计项
+ * @param [category] 类别
+ * @param [totalCount] 总数
+ * @param [completedCount] 已完成计数
+ * @param [typeRes] 类型res
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CategoryStatisticItem(
+ category: Category,
+ totalCount: Int,
+ completedCount: Int,
+ typeRes: StringResource,
+) {
+ if (totalCount == 0) return
+
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // 分类颜色指示器
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .clip(CircleShape)
+ .background(category.color.backgroundColor),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = category.icon.icon,
+ contentDescription = stringResource(category.icon.displayNameRes),
+ tint = category.color.iconColor,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+ // 分类信息
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
+ Row() {
+ Text(
+ text = category.name,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Medium,
+ color = category.color.textColor
+ )
+ Spacer(modifier = Modifier.width(6.dp))
+
+ Chip(
+ textRes = category.type.displayNameRes,
+ )
+ }
+
+ Text(
+ text = "${stringResource(typeRes)}: $completedCount/$totalCount",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ // 进度条
+ Column(
+ horizontalAlignment = Alignment.End
+ ) {
+ val progress = if (totalCount > 0) completedCount.toFloat() / totalCount else 0f
+
+ Text(
+ text = "${(progress * 100).toInt()}%",
+ style = MaterialTheme.typography.labelMedium,
+ fontWeight = FontWeight.Medium,
+ color = category.color.textColor
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ LinearProgressIndicator(
+ progress = { progress },
+ modifier = Modifier
+ .width(80.dp)
+ .height(6.dp)
+ .clip(RoundedCornerShape(3.dp)),
+ color = category.color.textColor,
+ trackColor = ProgressIndicatorDefaults.linearTrackColor,
+ strokeCap = ProgressIndicatorDefaults.LinearStrokeCap,
+ )
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(10.dp))
+}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/components/StatisticCard.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/components/StatisticCard.kt
new file mode 100644
index 0000000..bbf0484
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/statistics/components/StatisticCard.kt
@@ -0,0 +1,76 @@
+package com.taskttl.presentation.features.statistics.components
+// 统计卡
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import org.jetbrains.compose.resources.StringResource
+import org.jetbrains.compose.resources.stringResource
+
+
+/**
+ * 统计卡
+ * @param [titleRes] 标题res
+ * @param [value] 价值
+ * @param [icon] 图标
+ * @param [color] 颜色
+ * @param [modifier] 修饰符
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun StatisticCard(
+ titleRes: StringResource,
+ value: String,
+ icon: ImageVector,
+ color: Color,
+ modifier: Modifier = Modifier,
+) {
+ Card(
+ modifier = modifier,
+ colors = CardDefaults.cardColors(containerColor = color.copy(alpha = 0.1f))
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = stringResource(titleRes),
+ tint = color,
+ modifier = Modifier.size(32.dp)
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = value,
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold,
+ color = color
+ )
+
+ Text(
+ text = stringResource(titleRes),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskScreen.kt
index 475f0b9..add800d 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskScreen.kt
@@ -34,6 +34,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
+import com.taskttl.core.ui.component.empty.Empty
+import com.taskttl.core.ui.component.empty.EmptyData
import com.taskttl.core.utils.ToastUtils
import com.taskttl.navigation.Routes
import com.taskttl.presentation.common.components.AppHeader
@@ -167,23 +169,10 @@ fun TaskScreen(
}
state.filteredTasks.isEmpty() -> {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
- Column(horizontalAlignment = Alignment.CenterHorizontally) {
- Text(
- text = stringResource(Res.string.text_no_tasks),
- style = MaterialTheme.typography.bodyLarge
- )
- Spacer(modifier = Modifier.height(8.dp))
- Text(
- text = stringResource(Res.string.text_add_task_hint),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- }
+ Empty(
+ message = Res.string.text_no_tasks,
+ subtitle = Res.string.text_add_task_hint
+ )
}
else -> {
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskState.kt
index 9b65725..756c586 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskState.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/task/list/TaskState.kt
@@ -149,6 +149,12 @@ sealed class TaskIntent {
* @constructor 创建[TaskEffect]
*/
sealed class TaskEffect {
+ /**
+ * 导航返回
+ * @author admin
+ * @date 2025/09/27
+ */
+ object NavigateBack : TaskEffect()
/**
* 显示消息
* @author admin
@@ -173,11 +179,4 @@ sealed class TaskEffect {
* @date 2025/09/27
*/
data object NavigateToEditTask : TaskEffect()
-
- /**
- * 导航返回
- * @author admin
- * @date 2025/09/27
- */
- object NavigateBack : TaskEffect()
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/LoginScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginScreen.kt
similarity index 53%
rename from composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/LoginScreen.kt
rename to composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginScreen.kt
index 6550a0e..06be901 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/LoginScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginScreen.kt
@@ -1,14 +1,11 @@
-package com.taskttl.presentation.features.auth
-
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.Image
+package com.taskttl.presentation.features.user.login
+// 登录屏幕
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -21,10 +18,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Facebook
import androidx.compose.material.icons.filled.Language
+import androidx.compose.material3.Card
+import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -39,17 +37,11 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.taskttl.core.utils.ToastUtils
-import com.taskttl.navigation.Routes
import com.taskttl.presentation.common.components.AppHeader
import com.taskttl.presentation.common.components.ErrorDialog
-import com.taskttl.presentation.features.task.list.TaskEffect
-import com.taskttl.presentation.features.task.list.TaskIntent
-import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.InternalResourceApi
-import org.jetbrains.compose.resources.painterResource
import org.koin.compose.viewmodel.koinViewModel
import taskttl.composeapp.generated.resources.Res
import taskttl.composeapp.generated.resources.app_name
@@ -63,23 +55,15 @@ import taskttl.composeapp.generated.resources.app_name
@Composable
fun LoginScreen(
onNavigateBack: () -> Unit,
- viewModel: AuthViewModel = koinViewModel(),
+ viewModel: LoginViewModel = koinViewModel(),
) {
-
val state by viewModel.state.collectAsState()
LaunchedEffect(Unit) {
viewModel.effects.collect { effect ->
when (effect) {
- is AuthEffect.ShowMessage -> {
- ToastUtils.show(effect.message)
- }
-
- // is TaskEffect.NavigateToTaskDetail -> {
- // navController.navigate(Routes.Main.Task.TaskDetail(effect.taskId))
- // }
-
- else -> {}
+ is LoginEffect.NavigateBack -> onNavigateBack.invoke()
+ is LoginEffect.ShowMessage -> ToastUtils.show(effect.message)
}
}
}
@@ -87,7 +71,7 @@ fun LoginScreen(
state.error?.let { error ->
ErrorDialog(
errorMessage = state.error,
- onDismiss = { viewModel.handleIntent(AuthIntent.ClearError) }
+ onDismiss = { viewModel.handleIntent(LoginIntent.ClearError) }
)
}
@@ -105,68 +89,86 @@ fun LoginScreen(
Spacer(modifier = Modifier.height(48.dp))
- Column(
- modifier = Modifier.fillMaxSize()
- .background(MaterialTheme.colorScheme.background)
- .padding(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
) {
- // 应用 Logo
- AsyncImage(
- model = Res.getUri("drawable/ic_launcher.png"),
- contentDescription = null,
- modifier = Modifier.size(120.dp),
- contentScale = ContentScale.Fit
- )
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ shape = RoundedCornerShape(28.dp),
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp, vertical = 32.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ AsyncImage(
+ model = Res.getUri("drawable/ic_launcher.png"),
+ contentDescription = null,
+ modifier = Modifier.size(120.dp),
+ contentScale = ContentScale.Fit
+ )
- Spacer(modifier = Modifier.height(16.dp))
+ Spacer(modifier = Modifier.height(16.dp))
- Text(
- text = "TaskTTL 登录",
- style = MaterialTheme.typography.headlineMedium,
- fontWeight = FontWeight.Bold,
- textAlign = TextAlign.Center
- )
+ Text(
+ text = "TaskTTL 登录",
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.Center
+ )
- Spacer(modifier = Modifier.height(8.dp))
+ Spacer(modifier = Modifier.height(8.dp))
- Text(
- text = "欢迎回来,请选择登录方式:",
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- textAlign = TextAlign.Center
- )
+ Text(
+ text = "欢迎回来,请选择登录方式。",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ textAlign = TextAlign.Center
+ )
- Spacer(modifier = Modifier.height(32.dp))
+ Spacer(modifier = Modifier.height(24.dp))
- // Google 登录按钮
- LoginButton(
- icon = Icons.Default.Language,
- text = "使用 Google 登录",
- backgroundColor = Color(0xFFDB4437),
- onClick = { viewModel.handleIntent(AuthIntent.LoginWithGoogle) }
- )
+ LoginButton(
+ icon = Icons.Default.Language,
+ text = "使用 Google 登录",
+ backgroundColor = Color(0xFFDB4437),
+ onClick = { viewModel.handleIntent(LoginIntent.LoginWithGoogle) }
+ )
- Spacer(modifier = Modifier.height(16.dp))
-
- // Facebook 登录按钮
- LoginButton(
- icon = Icons.Default.Facebook,
- text = "使用 Facebook 登录",
- backgroundColor = Color(0xFF1877F2),
- onClick = { viewModel.handleIntent(AuthIntent.LoginWithFacebook) }
- )
-
- Spacer(modifier = Modifier.weight(1f))
+ Spacer(modifier = Modifier.height(16.dp))
+ LoginButton(
+ icon = Icons.Default.Facebook,
+ text = "使用 Facebook 登录",
+ backgroundColor = Color(0xFF1877F2),
+ onClick = { viewModel.handleIntent(LoginIntent.LoginWithFacebook) }
+ )
+ }
+ }
Text(
text = "© DevTTL Team. All rights reserved.",
style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
- modifier = Modifier.padding(bottom = 8.dp)
+ modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 32.dp)
+ .fillMaxWidth()
)
}
+ }
+ if (state.isLoading) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.35f)),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
+ }
}
}
}
@@ -208,38 +210,3 @@ private fun LoginButton(
)
}
}
-
-@Composable
-private fun AuthOutlineButton(
- text: String,
- onClick: () -> Unit,
- iconRes: DrawableResource,
- modifier: Modifier = Modifier,
-) {
- OutlinedButton(
- modifier = modifier
- .fillMaxWidth()
- .padding(horizontal = 24.dp),
- contentPadding = PaddingValues(16.dp),
- border = BorderStroke(width = 1.dp, color = Color.Black),
- onClick = onClick
- ) {
- Row(
- modifier = Modifier.align(Alignment.CenterVertically),
- ) {
- Image(
- painter = painterResource(iconRes),
- modifier = Modifier.size(24.dp),
- contentDescription = "",
- )
- Text(
- text = text,
- modifier = Modifier
- .padding(start = 12.dp)
- .align(Alignment.CenterVertically),
- fontSize = 16.sp,
- color = Color.Black,
- )
- }
- }
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/AuthState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginState.kt
similarity index 57%
rename from composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/AuthState.kt
rename to composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginState.kt
index 01f1878..884dcaf 100644
--- a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/auth/AuthState.kt
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginState.kt
@@ -1,19 +1,18 @@
-package com.taskttl.presentation.features.auth
-
+package com.taskttl.presentation.features.user.login
+// 身份验证状态
import com.taskttl.core.base.BaseState
-import com.taskttl.presentation.features.task.list.TaskEffect
/**
* 身份验证状态
* @author admin
* @date 2025/10/26
- * @constructor 创建[AuthState]
+ * @constructor 创建[LoginState]
* @param [isLoading] 正在加载
* @param [isProcessing] 正在处理
* @param [error] 错误
*/
-data class AuthState(
+data class LoginState(
override val isLoading: Boolean = false,
override val isProcessing: Boolean = false,
override val error: String? = null,
@@ -24,35 +23,42 @@ data class AuthState(
* 身份验证意图
* @author admin
* @date 2025/10/26
- * @constructor 创建[AuthIntent]
+ * @constructor 创建[LoginIntent]
*/
-sealed class AuthIntent {
+sealed class LoginIntent {
/**
* 使用谷歌登录
* @author admin
* @date 2025/10/26
*/
- object LoginWithGoogle: AuthIntent()
+ object LoginWithGoogle : LoginIntent()
/**
* 使用脸书登录
* @author admin
* @date 2025/10/26
*/
- object LoginWithFacebook: AuthIntent()
+ object LoginWithFacebook : LoginIntent()
- object ClearError: AuthIntent()
+ object ClearError : LoginIntent()
- object Logout : AuthIntent()
+ object Logout : LoginIntent()
}
/**
* 身份验证效果
* @author admin
* @date 2025/10/26
- * @constructor 创建[AuthEffect]
+ * @constructor 创建[LoginEffect]
*/
-sealed class AuthEffect {
+sealed class LoginEffect {
+ /**
+ * 导航返回
+ * @author admin
+ * @date 2025/11/25
+ */
+ object NavigateBack : LoginEffect()
+
/**
* 显示消息
* @author admin
@@ -60,5 +66,5 @@ sealed class AuthEffect {
* @constructor 创建[ShowMessage]
* @param [message] 消息
*/
- data class ShowMessage(val message: String) : AuthEffect()
+ data class ShowMessage(val message: String) : LoginEffect()
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginViewModel.kt
new file mode 100644
index 0000000..99110d0
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/login/LoginViewModel.kt
@@ -0,0 +1,104 @@
+package com.taskttl.presentation.features.user.login
+
+import androidx.compose.runtime.Stable
+import androidx.lifecycle.viewModelScope
+import com.taskttl.core.auth.AuthManager
+import com.taskttl.core.base.BaseViewModel
+import com.taskttl.data.source.remote.api.TaskTTLApi
+import com.taskttl.data.source.remote.dto.response.AuthResult
+import com.taskttl.domain.repository.AuthRepository
+import kotlinx.coroutines.launch
+import kotlin.time.ExperimentalTime
+
+/**
+ * 身份验证视图模型
+ * @author admin
+ * @date 2025/10/26
+ * @constructor 创建[LoginViewModel]
+ */
+@Stable
+class LoginViewModel(private val authRepository: AuthRepository) :
+ BaseViewModel(LoginState()) {
+
+
+ public override fun handleIntent(intent: LoginIntent) {
+ when (intent) {
+ is LoginIntent.LoginWithGoogle -> onLoginWithGoogle()
+ is LoginIntent.LoginWithFacebook -> onLoginWithFacebook()
+ is LoginIntent.ClearError -> clearError()
+ is LoginIntent.Logout -> {}
+ }
+ }
+
+ private fun setLoading(loading: Boolean) {
+ updateState { copy(isLoading = loading, isProcessing = false, error = null) }
+ }
+
+ /**
+ * 清除错误
+ */
+ private fun clearError() {
+ updateState { copy(error = null) }
+ }
+
+ /**
+ * 统一错误展示
+ */
+ private fun showError(message: String) {
+ updateState { copy(error = message) }
+ sendEvent(LoginEffect.ShowMessage(message))
+ }
+
+ /**
+ * 使用谷歌登录
+ */
+ private fun onLoginWithGoogle() {
+ if (state.value.isLoading) return
+ setLoading(true)
+ viewModelScope.launch {
+ try {
+ when (val result = authRepository.loginWithGoogle()) {
+ is AuthResult.Success -> handleGoogleLoginSuccess(result)
+ is AuthResult.Canceled -> showError("Login canceled")
+ is AuthResult.Error -> showError(result.message ?: "Unknown error")
+ }
+ } finally {
+ setLoading(false)
+ }
+ }
+ }
+
+ /**
+ * 使用脸书登录
+ */
+ private fun onLoginWithFacebook() {
+ logDebug("Facebook 登录触发")
+ viewModelScope.launch {
+ val result = authRepository.loginWithFacebook()
+ }
+ }
+
+ /**
+ * 处理谷歌登录成功
+ * @param [authResult] 登录结果
+ */
+ @OptIn(ExperimentalTime::class)
+ private suspend fun handleGoogleLoginSuccess(authResult: AuthResult.Success) {
+ if (authResult.account.isEmpty) {
+ showError("Login canceled")
+ return
+ }
+ runCatching {
+ TaskTTLApi.thirdPartyLogin(authResult.account)
+ }.onSuccess { result ->
+ AuthManager.updateToken(result.toTokenInfo())
+ AuthManager.updateUserInfo(result.toUserInfo())
+ sendEvent(LoginEffect.ShowMessage("登录成功"))
+ sendEvent(LoginEffect.NavigateBack)
+ }.onFailure { e ->
+ val message = e.message ?: "登录失败,请稍后重试"
+ logDebug("Third party login failed: $message")
+ showError(message)
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileScreen.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileScreen.kt
new file mode 100644
index 0000000..c0575eb
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileScreen.kt
@@ -0,0 +1,241 @@
+package com.taskttl.presentation.features.user.userProfile
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Logout
+import androidx.compose.material.icons.filled.ContentCopy
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.material.icons.filled.WarningAmber
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.taskttl.core.utils.ToastUtils
+import com.taskttl.presentation.common.components.AppHeader
+import com.taskttl.presentation.features.user.userProfile.components.AvatarView
+import com.taskttl.presentation.features.user.userProfile.components.EmailRow
+import com.taskttl.presentation.features.user.userProfile.components.InfoRow
+import com.taskttl.presentation.features.user.userProfile.components.NicknameEditor
+import org.koin.compose.viewmodel.koinViewModel
+import taskttl.composeapp.generated.resources.Res
+import taskttl.composeapp.generated.resources.title_app_settings
+
+@Composable
+fun UserProfileScreen(
+ onNavigateBack: () -> Unit,
+ viewModel: UserProfileViewModel = koinViewModel(),
+) {
+ val state by viewModel.state.collectAsState()
+
+ if (state.userInfo == null) {
+ return
+ }
+
+ LaunchedEffect(Unit) {
+ viewModel.effects.collect { effect ->
+ when (effect) {
+ is UserProfileEffect.NavigateBack -> onNavigateBack.invoke()
+ is UserProfileEffect.ShowMessage -> ToastUtils.show(effect.message)
+ }
+ }
+ }
+
+ val clipboardManager = LocalClipboardManager.current
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.surface)
+ ) {
+ Column(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ AppHeader(
+ title = Res.string.title_app_settings,
+ showBack = true,
+ onBackClick = { onNavigateBack.invoke() },
+ trailingIcon = Icons.Default.Person,
+ )
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)
+ .padding(16.dp)
+ ) {
+ // 顶部用户卡片
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ // 头像
+ AvatarView(
+ name = state.userInfo!!.nickname,
+ avatarUrl = state.userInfo?.avatar,
+ onClick = { viewModel.handleIntent(UserProfileIntent.ChangeAvatar) }
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ // 昵称编辑
+ NicknameEditor(
+ nickname = state.userInfo!!.nickname,
+ onNicknameChanged = {
+ viewModel.handleIntent(UserProfileIntent.UpdateNickname(it))
+ },
+ onSave = {
+ viewModel.handleIntent(UserProfileIntent.SaveNickname)
+ }
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // 邮箱展示
+ EmailRow(email = state.userInfo!!.email)
+ }
+ }
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ // 详细信息列表(ID / 账号信息)
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ Text(
+ text = "账户信息",
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 14.sp,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+
+ // 用户 ID
+ InfoRow(
+ label = "用户 ID",
+ value = state.userInfo!!.userId.toString(),
+ trailing = {
+ Icon(
+ imageVector = Icons.Default.ContentCopy,
+ contentDescription = "复制用户 ID",
+ modifier = Modifier
+ .size(18.dp)
+ .clickable {
+ // clipboardManager.setText(annotatedString)
+ viewModel.handleIntent(UserProfileIntent.ShowToast("已复制用户 ID"))
+ },
+ tint = LocalContentColor.current
+ )
+ }
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // 用户昵称(只展示,真正编辑在上面)
+ InfoRow(
+ label = "用户昵称",
+ value = state.userInfo!!.nickname
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // 邮箱
+ InfoRow(
+ label = "邮箱",
+ value = state.userInfo!!.email
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ // 底部操作按钮
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.Bottom,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Button(
+ onClick = { viewModel.handleIntent(UserProfileIntent.Logout) },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(48.dp),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.Logout,
+ contentDescription = null
+ )
+ Spacer(modifier = Modifier.size(8.dp))
+ Text("退出登录")
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ OutlinedButton(
+ onClick = {
+ viewModel.handleIntent(UserProfileIntent.DeleteAccount)
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(48.dp),
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.outlinedButtonColors(
+ contentColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Icon(
+ imageVector = Icons.Default.WarningAmber,
+ contentDescription = null
+ )
+ Spacer(modifier = Modifier.size(8.dp))
+ Text("删除账号")
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileState.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileState.kt
new file mode 100644
index 0000000..9381dc4
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileState.kt
@@ -0,0 +1,55 @@
+package com.taskttl.presentation.features.user.userProfile
+
+import com.taskttl.core.auth.AuthManager
+import com.taskttl.core.base.BaseState
+import com.taskttl.domain.model.UserInfo
+
+/**
+ * 用户配置文件状态
+ * @author DevTTL
+ * @date 2025/12/03
+ * @constructor 创建[UserProfileState]
+ * @param [userInfo] 用户信息
+ */
+data class UserProfileState(
+ val userInfo: UserInfo? = AuthManager.userInfo.value,
+): BaseState()
+
+/**
+ * 用户配置文件意图
+ * @author DevTTL
+ * @date 2025/12/03
+ * @constructor 创建[UserProfileIntent]
+ */
+sealed interface UserProfileIntent {
+ data class UpdateNickname(val value: String) : UserProfileIntent
+ data object SaveNickname : UserProfileIntent
+ data object ChangeAvatar : UserProfileIntent
+ data object Logout : UserProfileIntent
+ data object DeleteAccount : UserProfileIntent
+ data class ShowToast(val message: String) : UserProfileIntent
+}
+
+/**
+ * 用户配置文件效果
+ * @author DevTTL
+ * @date 2025/12/03
+ * @constructor 创建[UserProfileEffect]
+ */
+sealed interface UserProfileEffect {
+ /**
+ * 导航返回
+ * @author admin
+ * @date 2025/09/27
+ */
+ object NavigateBack : UserProfileEffect
+
+ /**
+ * 显示消息
+ * @author admin
+ * @date 2025/09/27
+ * @constructor 创建[ShowMessage]
+ * @param [message] 消息
+ */
+ data class ShowMessage(val message: String) : UserProfileEffect
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileViewModel.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileViewModel.kt
new file mode 100644
index 0000000..f0d1315
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/UserProfileViewModel.kt
@@ -0,0 +1,37 @@
+package com.taskttl.presentation.features.user.userProfile
+
+import androidx.compose.runtime.Stable
+import androidx.lifecycle.viewModelScope
+import com.taskttl.core.auth.AuthManager
+import com.taskttl.core.base.BaseViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+/**
+ * 用户配置文件视图模型
+ * @author DevTTL
+ * @date 2025/12/03
+ * @constructor 创建[UserProfileViewModel]
+ */
+@Stable
+class UserProfileViewModel() :
+ BaseViewModel(UserProfileState()) {
+
+ init {
+ viewModelScope.launch(Dispatchers.Default) {
+ AuthManager.userInfo.collect { updateState { copy(userInfo = it) } }
+ }
+ }
+
+ public override fun handleIntent(intent: UserProfileIntent) {
+ when (intent) {
+ is UserProfileIntent.Logout -> {}
+ is UserProfileIntent.ShowToast -> {}
+ is UserProfileIntent.UpdateNickname -> {}
+ is UserProfileIntent.DeleteAccount -> {}
+ is UserProfileIntent.ChangeAvatar -> {}
+ is UserProfileIntent.SaveNickname -> {}
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/AvatarView.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/AvatarView.kt
new file mode 100644
index 0000000..e284aee
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/AvatarView.kt
@@ -0,0 +1,66 @@
+package com.taskttl.presentation.features.user.userProfile.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+/**
+ * 头像视图
+ * @param [name] 名字
+ * @param [avatarUrl] 头像链接
+ * @param [onClick] 点击时
+ */
+@Composable
+fun AvatarView(
+ name: String,
+ avatarUrl: String?,
+ onClick: () -> Unit,
+) {
+ Box(
+ modifier = Modifier
+ .size(80.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.12f))
+ .clickable { onClick() },
+ contentAlignment = Alignment.Center
+ ) {
+ // 如果你有真实头像,这里改成 Image(...)
+ Text(
+ text = name.takeIf { it.isNotBlank() }?.first()?.uppercase() ?: "U",
+ fontSize = 32.sp,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.primary
+ )
+
+ // 右下角一个小铅笔提示可编辑
+ Box(
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .size(24.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.surface),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.Edit,
+ contentDescription = "编辑头像",
+ modifier = Modifier.size(16.dp),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/EmailRow.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/EmailRow.kt
new file mode 100644
index 0000000..8eb5e31
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/EmailRow.kt
@@ -0,0 +1,41 @@
+package com.taskttl.presentation.features.user.userProfile.components
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Email
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+/**
+ * 电子邮件行
+ * @param [email] 电子邮件
+ */
+@Composable
+fun EmailRow(email: String) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Default.Email,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Spacer(modifier = Modifier.size(4.dp))
+ Text(
+ text = email.ifBlank { "未绑定邮箱" },
+ fontSize = 14.sp,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/InfoRow.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/InfoRow.kt
new file mode 100644
index 0000000..796ec3c
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/InfoRow.kt
@@ -0,0 +1,57 @@
+package com.taskttl.presentation.features.user.userProfile.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+/**
+ * 信息行
+ * @param [label] 标签
+ * @param [value] 价值
+ * @param [trailing] 拖尾
+ */
+@Composable
+fun InfoRow(
+ label: String,
+ value: String,
+ trailing: (@Composable () -> Unit)? = null,
+) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = label,
+ fontSize = 13.sp,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ Spacer(modifier = Modifier.height(2.dp))
+ Text(
+ text = value.ifBlank { "-" },
+ fontSize = 15.sp,
+ color = MaterialTheme.colorScheme.onSurface,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+
+ if (trailing != null) {
+ Spacer(modifier = Modifier.size(8.dp))
+ trailing()
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/NicknameEditor.kt b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/NicknameEditor.kt
new file mode 100644
index 0000000..30fff85
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/taskttl/presentation/features/user/userProfile/components/NicknameEditor.kt
@@ -0,0 +1,55 @@
+package com.taskttl.presentation.features.user.userProfile.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+/**
+ * 昵称编辑器
+ * @param [nickname] 昵称
+ * @param [onNicknameChanged] 昵称已更改
+ * @param [onSave] 关于保存
+ */
+@Composable
+fun NicknameEditor(
+ nickname: String,
+ onNicknameChanged: (String) -> Unit,
+ onSave: () -> Unit,
+) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ OutlinedTextField(
+ value = nickname,
+ onValueChange = onNicknameChanged,
+ singleLine = true,
+ label = { Text("昵称") },
+ trailingIcon = {
+ Icon(
+ imageVector = Icons.Default.Edit,
+ contentDescription = null
+ )
+ }
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Button(
+ onClick = onSave,
+ shape = RoundedCornerShape(999.dp),
+ contentPadding = ButtonDefaults.ContentPadding
+ ) {
+ Text("保存昵称")
+ }
+ }
+}
diff --git a/composeApp/src/iosMain/kotlin/com/taskttl/MainViewController.kt b/composeApp/src/iosMain/kotlin/com/taskttl/MainViewController.kt
index a60e213..d5f1f00 100644
--- a/composeApp/src/iosMain/kotlin/com/taskttl/MainViewController.kt
+++ b/composeApp/src/iosMain/kotlin/com/taskttl/MainViewController.kt
@@ -1,5 +1,6 @@
package com.taskttl
import androidx.compose.ui.window.ComposeUIViewController
+import com.taskttl.app.App
fun MainViewController() = ComposeUIViewController { App() }
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 35206f9..55984e3 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,41 +1,50 @@
[versions]
-agp = "8.13.1"
-androidx-activity = "1.11.0"
+agp = "8.13.2"
+androidx-activity = "1.12.4"
androidx-appcompat = "1.7.1"
androidx-constraintlayout = "2.2.1"
androidx-core = "1.17.0"
androidx-espresso = "3.7.0"
androidx-lifecycle = "2.9.6"
androidx-testExt = "1.3.0"
-composeHotReload = "1.0.0-rc02"
-composeMultiplatform = "1.10.0-beta01"
+componentsResources = "1.11.0-alpha02"
+composeHotReload = "1.0.0"
+composeMultiplatform = "1.11.0-alpha02"
+foundation = "1.11.0-alpha02"
junit = "4.13.2"
-kotlin = "2.2.21"
+kotlin = "2.3.10"
kotlinx-coroutines = "1.10.2"
+material3 = "1.9.0"
+runtime = "1.11.0-alpha02"
splashscreen = "1.2.0"
-navigationCompose = "2.9.1"
-koin = "4.1.1"
-ktor = "3.3.2"
+navigationCompose = "2.9.2"
+androidxNavigation3UI = "1.0.0-alpha04"
+androidxNavigation3Material = "1.3.0-alpha01"
+koin = "4.2.0-RC1"
+ktor = "3.4.0"
coil3 = "3.3.0"
kotlinx-datetime = "0.7.1"
icons = "1.7.3"
google = "4.4.4"
-credentials = "1.6.0-beta03"
-googleid = "1.1.1"
-firebase = "34.6.0"
+credentials = "1.6.0-rc01"
+googleid = "1.2.0"
+firebase = "34.9.0"
facebook = "18.1.3"
-playServicesAds = "18.2.0"
-kotlinx-serialization = "1.9.0"
+identifier = "1.0.0-alpha05"
+kotlinx-serialization = "1.10.0"
settings = "1.3.0"
-sqlite = "2.6.1"
-room = "2.8.3"
-ksp = "2.3.0"
+sqlite = "2.6.2"
+room = "2.8.4"
+ksp = "2.3.2"
-work = "2.11.0"
+ui = "1.11.0-alpha02"
+uiTooling = "1.11.0-alpha02"
+uiToolingPreviewVersion = "1.11.0-alpha02"
+work = "2.11.1"
# 环境
config-appName = "TaskTTL"
@@ -54,6 +63,12 @@ android-facebookClientToken = "15db47cd9d8d35ccaa49f43e30beefaf"
[libraries]
+components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "componentsResources" }
+foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "foundation" }
+jetbrains-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "uiToolingPreviewVersion" }
+runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "runtime" }
+ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "ui" }
+ui-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "uiTooling" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { module = "junit:junit", version.ref = "junit" }
@@ -72,13 +87,17 @@ kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-s
androidx-splashscreen = {module = "androidx.core:core-splashscreen" , version.ref = "splashscreen"}
# 导航
+material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "material3" }
navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
+#androidx-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "androidxNavigation3UI" }
+#androidx-navigation3-material3-adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation3", version.ref = "androidxNavigation3Material" }
# koin 依赖注入
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
koin-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
+koin-navigation3 = {module = "io.insert-koin:koin-compose-navigation3", version.ref = "koin"}
# ktor 网络请求
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
@@ -92,7 +111,7 @@ ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "k
# coil3
coil3-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil3" }
coil3-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil3" }
-coil3-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil3" }
+#coil3-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil3" }
coil3-network-ktor3 = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil3" }
# 时间
@@ -123,7 +142,7 @@ androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref =
androidx-room-sqlite-wrapper = { module = "androidx.room:room-sqlite-wrapper", version.ref = "room" }
# 谷歌Ads
-android-play-services-ads-identifier = { module = "com.google.android.gms:play-services-ads-identifier", version.ref = "playServicesAds" }
+androidx-ads-identifier = { module = "androidx.ads:ads-identifier", version.ref = "identifier" }
# 安卓任务
androidx-work = { module = "androidx.work:work-runtime-ktx", version.ref = "work" }