feat: 复刻小程序四大主页面与底部导航(UI 优先,本地 mock)
This commit is contained in:
@@ -5,3 +5,7 @@ plugins {
|
||||
android {
|
||||
namespace = "com.mingyuan.android_client.feature.profile"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,216 @@
|
||||
package com.mingyuan.android_client.feature.profile
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
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
|
||||
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.material3.AlertDialog
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.mingyuan.android_client.core.designsystem.component.Avatar
|
||||
import com.mingyuan.android_client.core.designsystem.component.ButtonVariant
|
||||
import com.mingyuan.android_client.core.designsystem.component.ErrorState
|
||||
import com.mingyuan.android_client.core.designsystem.component.LabeledRow
|
||||
import com.mingyuan.android_client.core.designsystem.component.MingyuanButton
|
||||
import com.mingyuan.android_client.core.designsystem.component.SectionCard
|
||||
import com.mingyuan.android_client.core.designsystem.component.SkeletonBox
|
||||
import com.mingyuan.android_client.core.designsystem.theme.BrandAmber
|
||||
import com.mingyuan.android_client.core.designsystem.theme.Danger
|
||||
import com.mingyuan.android_client.core.designsystem.theme.MutedText
|
||||
import com.mingyuan.android_client.core.designsystem.theme.NeutralChip
|
||||
import com.mingyuan.android_client.core.designsystem.theme.PageBackground
|
||||
import com.mingyuan.android_client.core.designsystem.theme.PrimaryText
|
||||
import com.mingyuan.android_client.core.model.ClassGroup
|
||||
import com.mingyuan.android_client.core.model.UserProfile
|
||||
|
||||
/** Navigation route for this feature, referenced by the app-level NavHost. */
|
||||
const val PROFILE_ROUTE = "profile"
|
||||
|
||||
@Composable
|
||||
fun ProfileScreen(modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(text = "Profile")
|
||||
fun ProfileScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ProfileViewModel = hiltViewModel(),
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
|
||||
when (val state = uiState) {
|
||||
is ProfileUiState.Loading -> ProfileSkeleton(modifier)
|
||||
is ProfileUiState.Error -> ErrorState(
|
||||
message = state.message,
|
||||
onRetry = {},
|
||||
modifier = modifier.padding(16.dp),
|
||||
)
|
||||
|
||||
is ProfileUiState.Success -> ProfileContent(
|
||||
profile = state.profile,
|
||||
onDelete = { showDeleteDialog = true },
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
if (showDeleteDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteDialog = false },
|
||||
title = { Text("注销账号") },
|
||||
text = { Text("确定要注销账号吗?此操作不可恢复。") },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showDeleteDialog = false
|
||||
viewModel.deleteAccount()
|
||||
},
|
||||
) {
|
||||
Text("确定", color = Danger)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDeleteDialog = false }) { Text("取消") }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileContent(
|
||||
profile: UserProfile,
|
||||
onDelete: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize().background(PageBackground),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
item { ProfileHeaderCard(profile) }
|
||||
item { InfoCard(profile) }
|
||||
if (profile.classGroups.isNotEmpty()) {
|
||||
item { ClassGroupSection(profile.classGroups) }
|
||||
}
|
||||
item {
|
||||
MingyuanButton(
|
||||
text = "注销账号",
|
||||
onClick = onDelete,
|
||||
variant = ButtonVariant.DANGER,
|
||||
fullWidth = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileHeaderCard(profile: UserProfile) {
|
||||
SectionCard {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box {
|
||||
Avatar(initial = profile.displayName, size = 56.dp)
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.size(20.dp)
|
||||
.background(BrandAmber, CircleShape)
|
||||
.border(2.dp, PageBackground, CircleShape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text("📷", fontSize = 10.sp)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Column(Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = profile.displayName,
|
||||
color = PrimaryText,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Box(
|
||||
Modifier
|
||||
.background(NeutralChip, RoundedCornerShape(50))
|
||||
.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||
) {
|
||||
Text(
|
||||
text = profile.role,
|
||||
color = BrandAmber,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(profile.studentId, color = MutedText, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoCard(profile: UserProfile) {
|
||||
SectionCard {
|
||||
Text("个人信息", color = PrimaryText, fontSize = 15.sp, fontWeight = FontWeight.Bold)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
LabeledRow("姓名", profile.name)
|
||||
LabeledRow("学号", profile.studentId)
|
||||
LabeledRow("手机号", profile.phone ?: "未绑定")
|
||||
LabeledRow("角色", profile.role)
|
||||
LabeledRow("小组", profile.group ?: "未分组")
|
||||
LabeledRow("登录方式", profile.loginType)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ClassGroupSection(classGroups: List<ClassGroup>) {
|
||||
SectionCard {
|
||||
Text("小组分组", color = PrimaryText, fontSize = 15.sp, fontWeight = FontWeight.Bold)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
classGroups.forEach { group ->
|
||||
LabeledRow(group.period, "${group.group} · ${group.role}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileSkeleton(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize().background(PageBackground).padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
SkeletonBox(Modifier.size(56.dp), cornerRadius = 28)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Column {
|
||||
SkeletonBox(Modifier.width(120.dp).height(20.dp))
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SkeletonBox(Modifier.width(80.dp).height(12.dp))
|
||||
}
|
||||
}
|
||||
SkeletonBox(Modifier.fillMaxWidth().height(200.dp), cornerRadius = 16)
|
||||
SkeletonBox(Modifier.fillMaxWidth().height(48.dp), cornerRadius = 50)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,43 @@
|
||||
package com.mingyuan.android_client.feature.profile
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.mingyuan.android_client.core.domain.repository.ProfileRepository
|
||||
import com.mingyuan.android_client.core.model.UserProfile
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
sealed interface ProfileUiState {
|
||||
data object Loading : ProfileUiState
|
||||
data class Success(val profile: UserProfile) : ProfileUiState
|
||||
data class Error(val message: String) : ProfileUiState
|
||||
}
|
||||
|
||||
@HiltViewModel
|
||||
class ProfileViewModel @Inject constructor() : ViewModel()
|
||||
class ProfileViewModel @Inject constructor(
|
||||
private val profileRepository: ProfileRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
val uiState: StateFlow<ProfileUiState> = profileRepository.observeProfile()
|
||||
.map<UserProfile, ProfileUiState>(ProfileUiState::Success)
|
||||
.onStart { emit(ProfileUiState.Loading) }
|
||||
.catch { emit(ProfileUiState.Error(it.message ?: "加载失败,请稍后再试")) }
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = ProfileUiState.Loading,
|
||||
)
|
||||
|
||||
fun deleteAccount() {
|
||||
viewModelScope.launch {
|
||||
profileRepository.deleteAccount()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user