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.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.material3.Text
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -25,9 +18,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp 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.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.ActionButton import com.aiosman.ravenow.ui.composables.ActionButton
import com.aiosman.ravenow.ui.composables.StatusBarSpacer import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.composables.TextInputField
import com.aiosman.ravenow.utils.PasswordValidator import com.aiosman.ravenow.utils.PasswordValidator
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -129,70 +121,19 @@ fun RemoveAccountScreen() {
color = appColors.text color = appColors.text
) )
} }
Text( TextInputField(
stringResource(R.string.remove_account_password_hint), modifier = Modifier.fillMaxWidth(),
fontSize = 16.sp, text = inputPassword,
modifier = Modifier onValueChange = {
.fillMaxWidth() inputPassword = it
.padding(top = 16.dp, bottom = 4.dp), if (passwordError != null) {
color = appColors.text 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)) Spacer(modifier = Modifier.weight(1f))
ActionButton( ActionButton(

View File

@@ -35,7 +35,10 @@ import androidx.compose.material.Text
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf 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.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.AppState 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.ScrollStrategy
import com.aiosman.ravenow.ui.composables.toolbar.rememberCollapsingToolbarScaffoldState import com.aiosman.ravenow.ui.composables.toolbar.rememberCollapsingToolbarScaffoldState
import com.aiosman.ravenow.ui.index.IndexViewModel 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.OtherProfileAction
import com.aiosman.ravenow.ui.index.tabs.profile.composable.SelfProfileAction import com.aiosman.ravenow.ui.index.tabs.profile.composable.SelfProfileAction
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsList import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsList
@@ -91,7 +96,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
fun ProfileV3( fun ProfileV3(
onUpdateBanner: ((Uri, File, Context) -> Unit)? = null, onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,
@@ -117,6 +122,9 @@ fun ProfileV3(
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues() val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
var minibarExpanded 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 context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val navController = LocalNavController.current val navController = LocalNavController.current
@@ -132,6 +140,7 @@ fun ProfileV3(
val refreshState = rememberPullRefreshState(model.refreshing, onRefresh = { val refreshState = rememberPullRefreshState(model.refreshing, onRefresh = {
model.loadProfile(pullRefresh = true) model.loadProfile(pullRefresh = true)
}) })
val agentMenuModalState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
var miniToolbarHeight by remember { mutableStateOf(0) } var miniToolbarHeight by remember { mutableStateOf(0) }
val density = LocalDensity.current val density = LocalDensity.current
val appTheme = LocalAppTheme.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( Box(
modifier = Modifier.pullRefresh(refreshState) 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 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.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -46,7 +50,8 @@ fun UserAgentsRow(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onMoreClick: () -> Unit = {}, onMoreClick: () -> Unit = {},
onAgentClick: (AgentEntity) -> Unit = {}, onAgentClick: (AgentEntity) -> Unit = {},
onAvatarClick: (AgentEntity) -> Unit = {} onAvatarClick: (AgentEntity) -> Unit = {},
onAgentLongClick: (AgentEntity) -> Unit = {}
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val viewModel: UserAgentsViewModel = viewModel(key = "UserAgentsViewModel_${userId ?: "self"}") val viewModel: UserAgentsViewModel = viewModel(key = "UserAgentsViewModel_${userId ?: "self"}")
@@ -130,7 +135,8 @@ fun UserAgentsRow(
AgentItem( AgentItem(
agent = agent, agent = agent,
onClick = { onAgentClick(agent) }, onClick = { onAgentClick(agent) },
onAvatarClick = { onAvatarClick(agent) } onAvatarClick = { onAvatarClick(agent) },
onLongClick = { onAgentLongClick(agent) }
) )
} }
@@ -148,25 +154,50 @@ fun UserAgentsRow(
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun AgentItem( private fun AgentItem(
agent: AgentEntity, agent: AgentEntity,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
onAvatarClick: () -> Unit = {} onAvatarClick: () -> Unit = {},
onLongClick: () -> Unit = {}
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier 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( Box(
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.clip(CircleShape) .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( CustomAsyncImage(
context = LocalContext.current, context = LocalContext.current,
@@ -189,9 +220,7 @@ private fun AgentItem(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier modifier = Modifier.width(48.dp)
.width(48.dp)
.noRippleClickable { onClick() }
) )
} }
} }