feat: 新增AI智能体主页
- 新增全新设计的AI智能体主页界面(`AiProfileV3`),包括个人信息卡片、操作按钮和动态列表。 - 添加相应的 `AiProfileViewModel` 来处理数据加载、关注/取关以及动态列表分页逻辑。 - 创建 `AiProfileWrap` 作为页面入口,并根据 `isAiAccount` 参数在导航中分发至新的AI主页。 - 在 `AccountProfileEntity` 和 `Account` 数据模型中增加了AI角色背景图字段(`aiRoleAvatar`, `aiRoleAvatarMedium`, `aiRoleAvatarLarge`)。
This commit is contained in:
@@ -64,6 +64,11 @@ data class AccountProfile(
|
|||||||
val aiAccount: Boolean,
|
val aiAccount: Boolean,
|
||||||
|
|
||||||
val chatAIId: String,
|
val chatAIId: String,
|
||||||
|
|
||||||
|
// AI角色背景图
|
||||||
|
val aiRoleAvatar: String? = null,
|
||||||
|
val aiRoleAvatarMedium: String? = null,
|
||||||
|
val aiRoleAvatarLarge: String? = null,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* 转换为Entity
|
* 转换为Entity
|
||||||
@@ -89,7 +94,16 @@ data class AccountProfile(
|
|||||||
chatToken = openImToken,
|
chatToken = openImToken,
|
||||||
aiAccount = aiAccount,
|
aiAccount = aiAccount,
|
||||||
rawAvatar = avatar,
|
rawAvatar = avatar,
|
||||||
chatAIId = chatAIId
|
chatAIId = chatAIId,
|
||||||
|
aiRoleAvatar = aiRoleAvatar?.let {
|
||||||
|
if (it.isNotEmpty()) "${ApiClient.BASE_SERVER}$it" else null
|
||||||
|
},
|
||||||
|
aiRoleAvatarMedium = aiRoleAvatarMedium?.let {
|
||||||
|
if (it.isNotEmpty()) "${ApiClient.BASE_SERVER}$it" else null
|
||||||
|
},
|
||||||
|
aiRoleAvatarLarge = aiRoleAvatarLarge?.let {
|
||||||
|
if (it.isNotEmpty()) "${ApiClient.BASE_SERVER}$it" else null
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ data class AccountProfileEntity(
|
|||||||
val rawAvatar: String,
|
val rawAvatar: String,
|
||||||
|
|
||||||
val chatAIId: String,
|
val chatAIId: String,
|
||||||
|
|
||||||
|
// AI角色背景图
|
||||||
|
val aiRoleAvatar: String? = null,
|
||||||
|
val aiRoleAvatarMedium: String? = null,
|
||||||
|
val aiRoleAvatarLarge: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ import com.aiosman.ravenow.ui.post.NewPostImageGridScreen
|
|||||||
import com.aiosman.ravenow.ui.post.NewPostScreen
|
import com.aiosman.ravenow.ui.post.NewPostScreen
|
||||||
import com.aiosman.ravenow.ui.post.PostScreen
|
import com.aiosman.ravenow.ui.post.PostScreen
|
||||||
import com.aiosman.ravenow.ui.profile.AccountProfileV2
|
import com.aiosman.ravenow.ui.profile.AccountProfileV2
|
||||||
|
import com.aiosman.ravenow.ui.profile.AiProfileWrap
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.vip.VipSelPage
|
import com.aiosman.ravenow.ui.index.tabs.profile.vip.VipSelPage
|
||||||
import com.aiosman.ravenow.ui.notification.NotificationScreen
|
import com.aiosman.ravenow.ui.notification.NotificationScreen
|
||||||
import com.aiosman.ravenow.ui.scan.ScanQrScreen
|
import com.aiosman.ravenow.ui.scan.ScanQrScreen
|
||||||
@@ -345,7 +346,13 @@ fun NavigationController(
|
|||||||
) {
|
) {
|
||||||
val id = it.arguments?.getString("id")!!
|
val id = it.arguments?.getString("id")!!
|
||||||
val isAiAccount = it.arguments?.getBoolean("isAiAccount") ?: false
|
val isAiAccount = it.arguments?.getBoolean("isAiAccount") ?: false
|
||||||
AccountProfileV2(id, isAiAccount)
|
|
||||||
|
// 根据isAiAccount参数分发到不同的Profile页面
|
||||||
|
if (isAiAccount) {
|
||||||
|
AiProfileWrap(id)
|
||||||
|
} else {
|
||||||
|
AccountProfileV2(id, isAiAccount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
|
|||||||
456
app/src/main/java/com/aiosman/ravenow/ui/profile/AiProfileV3.kt
Normal file
456
app/src/main/java/com/aiosman/ravenow/ui/profile/AiProfileV3.kt
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
package com.aiosman.ravenow.ui.profile
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
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.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
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.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.ravenow.AppState
|
||||||
|
import com.aiosman.ravenow.GuestLoginCheckOut
|
||||||
|
import com.aiosman.ravenow.GuestLoginCheckOutScene
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import com.aiosman.ravenow.ui.navigateToPost
|
||||||
|
import java.text.NumberFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AiProfileV3(
|
||||||
|
profile: AccountProfileEntity?,
|
||||||
|
moments: List<MomentEntity>,
|
||||||
|
postCount: Long? = null,
|
||||||
|
isSelf: Boolean = false,
|
||||||
|
onFollowClick: () -> Unit = {},
|
||||||
|
onChatClick: () -> Unit = {},
|
||||||
|
onShareClick: () -> Unit = {},
|
||||||
|
onLoadMore: () -> Unit = {},
|
||||||
|
onComment: (MomentEntity) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(appColors.background)
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
// 顶部状态栏间距
|
||||||
|
item {
|
||||||
|
val statusBarPadding = WindowInsets.systemBars.asPaddingValues()
|
||||||
|
Spacer(modifier = Modifier.height(statusBarPadding.calculateTopPadding()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI大卡片
|
||||||
|
item {
|
||||||
|
AiProfileCard(
|
||||||
|
profile = profile,
|
||||||
|
postCount = postCount,
|
||||||
|
numberFormat = numberFormat
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 三按钮交互区
|
||||||
|
if (!isSelf) {
|
||||||
|
item {
|
||||||
|
AiProfileActions(
|
||||||
|
profile = profile,
|
||||||
|
onFollowClick = onFollowClick,
|
||||||
|
onChatClick = onChatClick,
|
||||||
|
onShareClick = onShareClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态Grid - 使用items来渲染每个动态
|
||||||
|
item {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(3),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(((moments.size / 3 + 1) * 130).dp) // 动态计算高度
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
userScrollEnabled = false // 禁用内部滚动
|
||||||
|
) {
|
||||||
|
itemsIndexed(moments) { idx, moment ->
|
||||||
|
if (moment.images.isNotEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.padding(2.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
onComment(moment)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = moment.images[0].thumbnail,
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
context = LocalContext.current
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部间距
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(100.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 顶部返回按钮
|
||||||
|
TopBar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TopBar() {
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color.Transparent)
|
||||||
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
|
.statusBarsPadding(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 返回按钮
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color.Black.copy(alpha = 0.3f))
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "←",
|
||||||
|
fontSize = 24.sp,
|
||||||
|
color = Color.White,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AiProfileCard(
|
||||||
|
profile: AccountProfileEntity?,
|
||||||
|
postCount: Long?,
|
||||||
|
numberFormat: NumberFormat
|
||||||
|
) {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp) // 添加四边边距
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(400.dp)
|
||||||
|
.clip(RoundedCornerShape(16.dp)) // 添加圆角
|
||||||
|
) {
|
||||||
|
// 背景图 - 使用aiRoleAvatar
|
||||||
|
val backgroundUrl = profile?.aiRoleAvatarLarge
|
||||||
|
?: profile?.aiRoleAvatarMedium
|
||||||
|
?: profile?.aiRoleAvatar
|
||||||
|
?: profile?.avatar
|
||||||
|
|
||||||
|
CustomAsyncImage(
|
||||||
|
context = LocalContext.current,
|
||||||
|
imageUrl = backgroundUrl ?: "",
|
||||||
|
contentDescription = "AI Background",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
|
||||||
|
// 底部渐变遮罩
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
Color.Black.copy(alpha = 0.7f)
|
||||||
|
),
|
||||||
|
startY = 200f,
|
||||||
|
endY = 1200f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 左下角内容
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.padding(20.dp),
|
||||||
|
horizontalAlignment = Alignment.Start
|
||||||
|
) {
|
||||||
|
// AI昵称 - 左对齐
|
||||||
|
Text(
|
||||||
|
text = profile?.nickName ?: "",
|
||||||
|
fontSize = 22.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
|
||||||
|
// AI简介 - 左对齐
|
||||||
|
if (!profile?.bio.isNullOrEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = profile?.bio ?: "",
|
||||||
|
fontSize = 13.sp,
|
||||||
|
color = Color.White.copy(alpha = 0.85f),
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
lineHeight = 18.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计数据行 - 左对齐
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(24.dp)
|
||||||
|
) {
|
||||||
|
StatItem(
|
||||||
|
count = postCount ?: 0L,
|
||||||
|
label = stringResource(R.string.posts),
|
||||||
|
numberFormat = numberFormat
|
||||||
|
)
|
||||||
|
StatItem(
|
||||||
|
count = profile?.followerCount?.toLong() ?: 0L,
|
||||||
|
label = stringResource(R.string.followers_upper),
|
||||||
|
numberFormat = numberFormat
|
||||||
|
)
|
||||||
|
StatItem(
|
||||||
|
count = profile?.followingCount?.toLong() ?: 0L,
|
||||||
|
label = stringResource(R.string.following_upper),
|
||||||
|
numberFormat = numberFormat
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StatItem(
|
||||||
|
count: Long,
|
||||||
|
label: String,
|
||||||
|
numberFormat: NumberFormat
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = formatNumber(count, numberFormat),
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.White.copy(alpha = 0.8f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatNumber(count: Long, numberFormat: NumberFormat): String {
|
||||||
|
return when {
|
||||||
|
count >= 1_000_000 -> String.format("%.1fM", count / 1_000_000.0)
|
||||||
|
count >= 1_000 -> String.format("%.1fK", count / 1_000.0)
|
||||||
|
else -> numberFormat.format(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AiProfileActions(
|
||||||
|
profile: AccountProfileEntity?,
|
||||||
|
onFollowClick: () -> Unit,
|
||||||
|
onChatClick: () -> Unit,
|
||||||
|
onShareClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
|
// 定义渐变色
|
||||||
|
val followGradient = Brush.horizontalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFF7c45ed),
|
||||||
|
Color(0x777c68ef)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
// Follow按钮
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.let { modifier ->
|
||||||
|
if (profile?.isFollowing == true) {
|
||||||
|
// 已关注状态 - 透明背景
|
||||||
|
modifier.background(Color.Transparent)
|
||||||
|
} else {
|
||||||
|
// 未关注状态 - 渐变背景
|
||||||
|
modifier.background(brush = followGradient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.let { modifier ->
|
||||||
|
if (profile?.isFollowing == true) {
|
||||||
|
modifier.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = appColors.text.copy(alpha = 0.3f),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
modifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.FOLLOW_USER)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
onFollowClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (profile?.isFollowing == true)
|
||||||
|
stringResource(R.string.follow_upper_had)
|
||||||
|
else
|
||||||
|
stringResource(R.string.follow_upper),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W900,
|
||||||
|
color = if (profile?.isFollowing == true) {
|
||||||
|
appColors.text.copy(alpha = 0.6f)
|
||||||
|
} else {
|
||||||
|
Color.White
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chat按钮
|
||||||
|
if (AppState.enableChat) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(appColors.nonActive)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
onChatClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.chat_upper),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W900,
|
||||||
|
color = appColors.text,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Share按钮
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(appColors.nonActive)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
onShareClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.share),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W900,
|
||||||
|
color = appColors.text,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.aiosman.ravenow.ui.profile
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.aiosman.ravenow.data.AccountService
|
||||||
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
|
import com.aiosman.ravenow.data.UserServiceImpl
|
||||||
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
|
import com.aiosman.ravenow.entity.MomentLoader
|
||||||
|
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
|
||||||
|
import com.aiosman.ravenow.event.FollowChangeEvent
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import org.greenrobot.eventbus.Subscribe
|
||||||
|
|
||||||
|
class AiProfileViewModel : ViewModel() {
|
||||||
|
var profileId by mutableStateOf(0)
|
||||||
|
val accountService: AccountService = AccountServiceImpl()
|
||||||
|
val userService = UserServiceImpl()
|
||||||
|
var profile by mutableStateOf<AccountProfileEntity?>(null)
|
||||||
|
var refreshing by mutableStateOf(false)
|
||||||
|
|
||||||
|
var momentLoader = MomentLoader().apply {
|
||||||
|
pageSize = 20
|
||||||
|
onListChanged = {
|
||||||
|
moments = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var moments by mutableStateOf<List<MomentEntity>>(listOf())
|
||||||
|
|
||||||
|
init {
|
||||||
|
EventBus.getDefault().register(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadProfile(id: String, pullRefresh: Boolean = false) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
if (pullRefresh) {
|
||||||
|
refreshing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
profile = userService.getUserProfile(id)
|
||||||
|
profileId = id.toInt()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AiProfileViewModel", "Error loading profile", e)
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshing = false
|
||||||
|
|
||||||
|
profile?.let {
|
||||||
|
try {
|
||||||
|
momentLoader.loadData(MomentLoaderExtraArgs(authorId = it.id))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AiProfileViewModel", "Error loading moments", e)
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadMoreMoment() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
profile?.let { profileData ->
|
||||||
|
try {
|
||||||
|
Log.d("AiProfileViewModel", "loadMoreMoment: 开始加载更多, 当前moments数量: ${moments.size}, hasNext: ${momentLoader.hasNext}")
|
||||||
|
momentLoader.loadMore(extra = MomentLoaderExtraArgs(authorId = profileData.id))
|
||||||
|
Log.d("AiProfileViewModel", "loadMoreMoment: 加载完成, 新的moments数量: ${moments.size}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AiProfileViewModel", "loadMoreMoment: ", e)
|
||||||
|
}
|
||||||
|
} ?: Log.w("AiProfileViewModel", "loadMoreMoment: profile为null,无法加载更多")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
fun onFollowChangeEvent(event: FollowChangeEvent) {
|
||||||
|
if (event.userId == profile?.id) {
|
||||||
|
profile = profile?.copy(
|
||||||
|
followerCount = profile!!.followerCount + if (event.isFollow) 1 else -1,
|
||||||
|
isFollowing = event.isFollow
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun followUser(userId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
userService.followUser(userId)
|
||||||
|
EventBus.getDefault().post(FollowChangeEvent(userId.toInt(), true))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AiProfileViewModel", "Error following user", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unFollowUser(userId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
userService.unFollowUser(userId)
|
||||||
|
EventBus.getDefault().post(FollowChangeEvent(userId.toInt(), false))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AiProfileViewModel", "Error unfollowing user", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val bio get() = profile?.bio ?: ""
|
||||||
|
val nickName get() = profile?.nickName ?: ""
|
||||||
|
val avatar get() = profile?.avatar
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
EventBus.getDefault().unregister(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.aiosman.ravenow.ui.profile
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.aiosman.ravenow.GuestLoginCheckOut
|
||||||
|
import com.aiosman.ravenow.GuestLoginCheckOutScene
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.data.UserServiceImpl
|
||||||
|
import com.aiosman.ravenow.exp.viewModelFactory
|
||||||
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
||||||
|
import com.aiosman.ravenow.ui.navigateToChatAi
|
||||||
|
import com.aiosman.ravenow.ui.navigateToPost
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AiProfileWrap(id: String) {
|
||||||
|
val model: AiProfileViewModel = viewModel(factory = viewModelFactory {
|
||||||
|
AiProfileViewModel()
|
||||||
|
}, key = "aiViewModel_${id}")
|
||||||
|
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
model.loadProfile(id)
|
||||||
|
MyProfileViewModel.loadProfile()
|
||||||
|
}
|
||||||
|
|
||||||
|
val isSelf = id == MyProfileViewModel.profile?.id.toString()
|
||||||
|
|
||||||
|
AiProfileV3(
|
||||||
|
profile = model.profile,
|
||||||
|
moments = model.moments,
|
||||||
|
postCount = model.momentLoader.total,
|
||||||
|
isSelf = isSelf,
|
||||||
|
onFollowClick = {
|
||||||
|
model.profile?.let {
|
||||||
|
if (it.isFollowing) {
|
||||||
|
model.unFollowUser(id)
|
||||||
|
} else {
|
||||||
|
model.followUser(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onChatClick = {
|
||||||
|
// 检查游客模式
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
model.profile?.let { profile ->
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
// 参考主页逻辑:使用chatAIId作为openId创建单聊并导航
|
||||||
|
// 创建单聊
|
||||||
|
MineAgentViewModel.createSingleChat(profile.chatAIId)
|
||||||
|
|
||||||
|
// 通过chatAIId获取完整的AI profile(类似主页的goToChatAi逻辑)
|
||||||
|
val userService = UserServiceImpl()
|
||||||
|
val aiProfile = userService.getUserProfileByOpenId(profile.chatAIId)
|
||||||
|
|
||||||
|
// 导航到AI聊天页面
|
||||||
|
navController.navigateToChatAi(aiProfile.id.toString())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AiProfileWrap", "Error navigating to AI chat", e)
|
||||||
|
// 如果获取失败,直接使用当前profile的id
|
||||||
|
navController.navigateToChatAi(profile.id.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShareClick = {
|
||||||
|
// TODO: 实现分享逻辑
|
||||||
|
Log.d("AiProfileWrap", "分享功能待实现")
|
||||||
|
},
|
||||||
|
onLoadMore = {
|
||||||
|
Log.d("AiProfileWrap", "onLoadMore被调用")
|
||||||
|
model.loadMoreMoment()
|
||||||
|
},
|
||||||
|
onComment = { moment ->
|
||||||
|
navController.navigateToPost(moment.id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user