From ab43f154f5abfb313695ed6eafdb8d08d63ccf8a Mon Sep 17 00:00:00 2001 From: AllenTom Date: Mon, 1 Sep 2025 17:14:22 +0800 Subject: [PATCH] =?UTF-8?q?Refactor:=20=E4=BC=98=E5=8C=96=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E4=BD=93=E6=93=8D=E4=BD=9C=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 个人主页智能体列表项增加长按操作,用于弹出操作菜单。 - 实现智能体操作菜单弹窗,提供删除等操作选项。 - 增加删除智能体确认对话框,防止误操作。 - 移除账号页面密码输入框重构为通用组件。 --- .../ravenow/ui/account/removeaccount.kt | 85 +------ .../ui/index/tabs/profile/ProfileV3.kt | 236 +++++++++++++++++- .../tabs/profile/composable/UserAgentsRow.kt | 43 +++- 3 files changed, 284 insertions(+), 80 deletions(-) diff --git a/app/src/main/java/com/aiosman/ravenow/ui/account/removeaccount.kt b/app/src/main/java/com/aiosman/ravenow/ui/account/removeaccount.kt index 7db6f26..44bcab0 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/account/removeaccount.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/account/removeaccount.kt @@ -4,18 +4,11 @@ import android.widget.Toast 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.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.foundation.text.BasicTextField import androidx.compose.material3.Text -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.text.TextStyle import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -25,9 +18,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -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 @@ -46,6 +37,7 @@ import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.comment.NoticeScreenHeader import com.aiosman.ravenow.ui.composables.ActionButton import com.aiosman.ravenow.ui.composables.StatusBarSpacer +import com.aiosman.ravenow.ui.composables.TextInputField import com.aiosman.ravenow.utils.PasswordValidator import kotlinx.coroutines.launch @@ -129,70 +121,19 @@ fun RemoveAccountScreen() { color = appColors.text ) } - Text( - stringResource(R.string.remove_account_password_hint), - fontSize = 16.sp, - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp, bottom = 4.dp), - color = appColors.text + TextInputField( + modifier = Modifier.fillMaxWidth(), + text = inputPassword, + onValueChange = { + inputPassword = it + if (passwordError != null) { + passwordError = null + } + }, + password = true, + hint = stringResource(R.string.remove_account_password_hint), + error = passwordError ) - Box( - modifier = Modifier - .fillMaxWidth() - .background( - appColors.inputBackground, - shape = RoundedCornerShape(24.dp) - ) - .padding(vertical = 16.dp, horizontal = 16.dp) - ){ - - BasicTextField( - value = inputPassword, - onValueChange = { - inputPassword = it - if (passwordError != null) { - passwordError = null - } - }, - modifier = Modifier.fillMaxWidth(), - textStyle = TextStyle(color = appColors.text), - cursorBrush = SolidColor(appColors.text) - ) - if (inputPassword.isEmpty()) { - Text( - "Please enter your correct password", - modifier = Modifier.padding(start = 5.dp), - color = appColors.inputHint, - fontWeight = FontWeight.W600 - ) - } - } - // 显示密码错误信息 - passwordError?.let { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - androidx.compose.foundation.Image( - painter = painterResource(id = R.mipmap.rider_pro_input_error), - contentDescription = "Error", - modifier = Modifier.size(8.dp) - ) - Spacer(modifier = Modifier.size(4.dp)) - - Text( - text = it, - color = androidx.compose.ui.graphics.Color.Red, - fontSize = 14.sp, - modifier = Modifier - .fillMaxWidth() - .padding(top = 4.dp) - ) - } - } Spacer(modifier = Modifier.weight(1f)) ActionButton( diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/ProfileV3.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/ProfileV3.kt index d1a05c2..a698d23 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/ProfileV3.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/ProfileV3.kt @@ -35,7 +35,10 @@ import androidx.compose.material.Text import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -57,6 +60,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.aiosman.ravenow.AppState @@ -78,6 +82,7 @@ import com.aiosman.ravenow.ui.composables.toolbar.CollapsingToolbarScaffold import com.aiosman.ravenow.ui.composables.toolbar.ScrollStrategy import com.aiosman.ravenow.ui.composables.toolbar.rememberCollapsingToolbarScaffoldState import com.aiosman.ravenow.ui.index.IndexViewModel +import com.aiosman.ravenow.ui.post.MenuActionItem import com.aiosman.ravenow.ui.index.tabs.profile.composable.OtherProfileAction import com.aiosman.ravenow.ui.index.tabs.profile.composable.SelfProfileAction import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsList @@ -91,7 +96,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.io.File -@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @Composable fun ProfileV3( onUpdateBanner: ((Uri, File, Context) -> Unit)? = null, @@ -117,6 +122,9 @@ fun ProfileV3( val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues() var expanded by remember { mutableStateOf(false) } var minibarExpanded by remember { mutableStateOf(false) } + var showAgentMenu by remember { mutableStateOf(false) } + var contextAgent by remember { mutableStateOf(null) } + var showDeleteConfirmDialog by remember { mutableStateOf(false) } val context = LocalContext.current val scope = rememberCoroutineScope() val navController = LocalNavController.current @@ -132,6 +140,7 @@ fun ProfileV3( val refreshState = rememberPullRefreshState(model.refreshing, onRefresh = { model.loadProfile(pullRefresh = true) }) + val agentMenuModalState = rememberModalBottomSheetState(skipPartiallyExpanded = true) var miniToolbarHeight by remember { mutableStateOf(0) } val density = LocalDensity.current val appTheme = LocalAppTheme.current @@ -205,6 +214,67 @@ fun ProfileV3( } } + + // Agent菜单弹窗 + if (showAgentMenu) { + Log.d("ProfileV3", "Showing agent menu for: ${contextAgent?.title}") + ModalBottomSheet( + onDismissRequest = { + showAgentMenu = false + }, + containerColor = AppColors.background, + sheetState = agentMenuModalState, + dragHandle = {}, + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + windowInsets = WindowInsets(0) + ) { + AgentMenuModal( + agent = contextAgent, + onDeleteClick = { + scope.launch { + agentMenuModalState.hide() + showAgentMenu = false + showDeleteConfirmDialog = true + } + }, + onCloseClick = { + scope.launch { + agentMenuModalState.hide() + showAgentMenu = false + } + }, + isSelf = isSelf + ) + } + } + + // 删除确认对话框 + if (showDeleteConfirmDialog) { + DeleteConfirmDialog( + agentName = contextAgent?.title ?: "", + onConfirm = { + // TODO: 实现删除逻辑 + contextAgent?.let { agent -> + // 调用删除API + scope.launch { + try { + // 这里应该调用删除智能体的API + // agentService.deleteAgent(agent.id) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + showDeleteConfirmDialog = false + contextAgent = null + }, + onDismiss = { + showDeleteConfirmDialog = false + contextAgent = null + } + ) + } + Box( modifier = Modifier.pullRefresh(refreshState) ) { @@ -418,6 +488,14 @@ fun ProfileV3( // 处理错误 } } + }, + onAgentLongClick = { agent -> + Log.d("ProfileV3", "onAgentLongClick called for agent: ${agent.title}, isSelf: $isSelf") + if (isSelf) { // 只有自己的智能体才能长按 + Log.d("ProfileV3", "Setting contextAgent and showing menu") + contextAgent = agent + showAgentMenu = true + } } ) } @@ -608,3 +686,159 @@ fun ProfileV3( } } + +/** + * Agent菜单弹窗 + */ +@Composable +fun AgentMenuModal( + agent: AgentEntity?, + onDeleteClick: () -> Unit = {}, + onCloseClick: () -> Unit = {}, + isSelf: Boolean = true +) { + val AppColors = LocalAppTheme.current + + Column( + modifier = Modifier + .fillMaxWidth() + .background(AppColors.background) + .padding(vertical = 24.dp, horizontal = 20.dp) + ) { + Text( + "智能体", + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = AppColors.text + ) + Spacer(modifier = Modifier.height(24.dp)) + + agent?.let { + Column( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(AppColors.nonActive) + .padding(8.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(24.dp) + .clip(CircleShape) + ) { + CustomAsyncImage( + context = LocalContext.current, + imageUrl = it.avatar, + modifier = Modifier.fillMaxSize(), + contentDescription = "Avatar", + defaultRes = R.mipmap.rider_pro_agent + ) + } + Spacer(modifier = Modifier.width(8.dp)) + Text( + it.title, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + color = AppColors.text + ) + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + it.desc.ifEmpty { "暂无描述" }, + maxLines = 2, + modifier = Modifier + .fillMaxWidth() + .padding(start = 32.dp), + overflow = TextOverflow.Ellipsis, + color = AppColors.text, + fontSize = 14.sp + ) + } + Spacer(modifier = Modifier.height(32.dp)) + } + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + if (isSelf) { + MenuActionItem( + icon = R.drawable.rider_pro_moment_delete, + text = "删除" + ) { + onDeleteClick() + } + Spacer(modifier = Modifier.width(48.dp)) + } + + MenuActionItem( + icon = R.drawable.rider_pro_more_horizon, + text = "更多" + ) { + // TODO: 实现更多功能 + onCloseClick() + } + } + Spacer(modifier = Modifier.height(48.dp)) + } +} + + + +/** + * 删除确认对话框 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DeleteConfirmDialog( + agentName: String, + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + val AppColors = LocalAppTheme.current + + androidx.compose.material3.AlertDialog( + onDismissRequest = onDismiss, + title = { + Text( + text = "确认删除", + color = AppColors.text, + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + }, + text = { + Text( + text = "确定要删除智能体「$agentName」吗?删除后无法恢复。", + color = AppColors.text, + fontSize = 14.sp + ) + }, + confirmButton = { + androidx.compose.material3.TextButton( + onClick = { + onConfirm() + } + ) { + Text( + "删除", + color = AppColors.error, + fontWeight = FontWeight.Bold + ) + } + }, + dismissButton = { + androidx.compose.material3.TextButton( + onClick = onDismiss + ) { + Text( + "取消", + color = AppColors.text + ) + } + }, + containerColor = AppColors.background + ) +} diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/composable/UserAgentsRow.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/composable/UserAgentsRow.kt index c09cbdc..eaac9dd 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/composable/UserAgentsRow.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/composable/UserAgentsRow.kt @@ -1,6 +1,10 @@ package com.aiosman.ravenow.ui.index.tabs.profile.composable +import android.util.Log +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -46,7 +50,8 @@ fun UserAgentsRow( modifier: Modifier = Modifier, onMoreClick: () -> Unit = {}, onAgentClick: (AgentEntity) -> Unit = {}, - onAvatarClick: (AgentEntity) -> Unit = {} + onAvatarClick: (AgentEntity) -> Unit = {}, + onAgentLongClick: (AgentEntity) -> Unit = {} ) { val AppColors = LocalAppTheme.current val viewModel: UserAgentsViewModel = viewModel(key = "UserAgentsViewModel_${userId ?: "self"}") @@ -130,7 +135,8 @@ fun UserAgentsRow( AgentItem( agent = agent, onClick = { onAgentClick(agent) }, - onAvatarClick = { onAvatarClick(agent) } + onAvatarClick = { onAvatarClick(agent) }, + onLongClick = { onAgentLongClick(agent) } ) } @@ -148,25 +154,50 @@ fun UserAgentsRow( } } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun AgentItem( agent: AgentEntity, modifier: Modifier = Modifier, onClick: () -> Unit = {}, - onAvatarClick: () -> Unit = {} + onAvatarClick: () -> Unit = {}, + onLongClick: () -> Unit = {} ) { val AppColors = LocalAppTheme.current Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier + .combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { + Log.d("AgentItem", "onClick triggered for agent: ${agent.title}") + onClick() + }, + onLongClick = { + Log.d("AgentItem", "onLongClick triggered for agent: ${agent.title}") + onLongClick() + } + ) ) { // 头像 Box( modifier = Modifier .size(48.dp) .clip(CircleShape) - .noRippleClickable { onAvatarClick() } + .combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { + Log.d("AgentItem", "Avatar clicked for agent: ${agent.title}") + onAvatarClick() + }, + onLongClick = { + Log.d("AgentItem", "Avatar long clicked for agent: ${agent.title}") + onLongClick() + } + ) ) { CustomAsyncImage( context = LocalContext.current, @@ -189,9 +220,7 @@ private fun AgentItem( textAlign = TextAlign.Center, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier - .width(48.dp) - .noRippleClickable { onClick() } + modifier = Modifier.width(48.dp) ) } }