Refactor: 优化智能体操作交互

- 个人主页智能体列表项增加长按操作,用于弹出操作菜单。
- 实现智能体操作菜单弹窗,提供删除等操作选项。
- 增加删除智能体确认对话框,防止误操作。
- 移除账号页面密码输入框重构为通用组件。
This commit is contained in:
2025-09-01 17:14:22 +08:00
parent 9c7c87722b
commit ab43f154f5
3 changed files with 284 additions and 80 deletions

View File

@@ -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
)
Box(
modifier = Modifier
.fillMaxWidth()
.background(
appColors.inputBackground,
shape = RoundedCornerShape(24.dp)
)
.padding(vertical = 16.dp, horizontal = 16.dp)
){
BasicTextField(
value = inputPassword,
TextInputField(
modifier = Modifier.fillMaxWidth(),
text = inputPassword,
onValueChange = {
inputPassword = it
if (passwordError != null) {
passwordError = null
}
},
modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle(color = appColors.text),
cursorBrush = SolidColor(appColors.text)
password = true,
hint = stringResource(R.string.remove_account_password_hint),
error = passwordError
)
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(

View File

@@ -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<AgentEntity?>(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
)
}

View File

@@ -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)
)
}
}