群成员显示;添加群成员UI;群资料设置UI
This commit is contained in:
@@ -417,15 +417,16 @@ interface AccountService {
|
|||||||
* @param page 页码
|
* @param page 页码
|
||||||
* @param pageSize 每页数量
|
* @param pageSize 每页数量
|
||||||
*/
|
*/
|
||||||
suspend fun getAgent(page: Int, pageSize: Int): retrofit2.Response<DataContainer<ListContainer<Agent>>>
|
suspend fun getAgent(page: Int, pageSize: Int, excludeRoomId: Int? = null, title: String? = null, desc: String? = null): retrofit2.Response<DataContainer<ListContainer<Agent>>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建群聊
|
* 创建群聊
|
||||||
* @param name 群聊名称
|
* @param name 群聊名称
|
||||||
* @param userIds 用户ID列表
|
* @param userIds 用户ID列表
|
||||||
* @param promptIds AI智能体ID列表
|
* @param promptIds AI智能体ID列表
|
||||||
|
* @param roomId 房间ID,如果提供则添加成员到现有群聊,否则创建新群聊
|
||||||
*/
|
*/
|
||||||
suspend fun createGroupChat(name: String, userIds: List<String>, promptIds: List<String>): retrofit2.Response<DataContainer<Unit>>
|
suspend fun createGroupChat(name: String, userIds: List<String>, promptIds: List<String>, roomId: Int? = null): retrofit2.Response<DataContainer<Unit>>
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountServiceImpl : AccountService {
|
class AccountServiceImpl : AccountService {
|
||||||
@@ -630,15 +631,15 @@ class AccountServiceImpl : AccountService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAgent(page: Int, pageSize: Int): retrofit2.Response<DataContainer<ListContainer<Agent>>> {
|
override suspend fun getAgent(page: Int, pageSize: Int, excludeRoomId: Int?, title: String?, desc: String?): retrofit2.Response<DataContainer<ListContainer<Agent>>> {
|
||||||
return ApiClient.api.getAgent(page, pageSize)
|
return ApiClient.api.getAgent(page, pageSize, excludeRoomId = excludeRoomId, title = title, desc = desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun createGroupChat(name: String, userIds: List<String>, promptIds: List<String>): retrofit2.Response<DataContainer<Unit>> {
|
override suspend fun createGroupChat(name: String, userIds: List<String>, promptIds: List<String>, roomId: Int?): retrofit2.Response<DataContainer<Unit>> {
|
||||||
val requestBody = com.aiosman.ravenow.data.api.CreateGroupChatRequestBody(
|
val requestBody = com.aiosman.ravenow.data.api.CreateGroupChatRequestBody(
|
||||||
name = name,
|
name = name,
|
||||||
userIds = userIds,
|
userIds = userIds,
|
||||||
promptIds = promptIds
|
promptIds = promptIds,
|
||||||
)
|
)
|
||||||
return ApiClient.api.createGroupChat(requestBody)
|
return ApiClient.api.createGroupChat(requestBody)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ interface UserService {
|
|||||||
page: Int = 1,
|
page: Int = 1,
|
||||||
nickname: String? = null,
|
nickname: String? = null,
|
||||||
followerId: Int? = null,
|
followerId: Int? = null,
|
||||||
followingId: Int? = null
|
followingId: Int? = null,
|
||||||
|
roomId: Int? = null
|
||||||
): ListContainer<AccountProfileEntity>
|
): ListContainer<AccountProfileEntity>
|
||||||
|
|
||||||
|
|
||||||
@@ -90,7 +91,8 @@ class UserServiceImpl : UserService {
|
|||||||
page: Int,
|
page: Int,
|
||||||
nickname: String?,
|
nickname: String?,
|
||||||
followerId: Int?,
|
followerId: Int?,
|
||||||
followingId: Int?
|
followingId: Int?,
|
||||||
|
roomId: Int?
|
||||||
): ListContainer<AccountProfileEntity> {
|
): ListContainer<AccountProfileEntity> {
|
||||||
val resp = ApiClient.api.getUsers(
|
val resp = ApiClient.api.getUsers(
|
||||||
page = page,
|
page = page,
|
||||||
@@ -98,7 +100,7 @@ class UserServiceImpl : UserService {
|
|||||||
search = nickname,
|
search = nickname,
|
||||||
followerId = followerId,
|
followerId = followerId,
|
||||||
followingId = followingId,
|
followingId = followingId,
|
||||||
includeAI = true
|
includeAI = true,
|
||||||
)
|
)
|
||||||
val body = resp.body() ?: throw ServiceException("Failed to get account")
|
val body = resp.body() ?: throw ServiceException("Failed to get account")
|
||||||
return ListContainer<AccountProfileEntity>(
|
return ListContainer<AccountProfileEntity>(
|
||||||
|
|||||||
@@ -1046,6 +1046,9 @@ interface RaveNowAPI {
|
|||||||
@Query("authorId") authorId: Int? = null,
|
@Query("authorId") authorId: Int? = null,
|
||||||
@Query("categoryIds") categoryIds: List<Int>? = null,
|
@Query("categoryIds") categoryIds: List<Int>? = null,
|
||||||
@Query("random") random: Int? = null,
|
@Query("random") random: Int? = null,
|
||||||
|
@Query("title") title: String? = null,
|
||||||
|
@Query("desc") desc: String? = null,
|
||||||
|
@Query("excludeRoomId") excludeRoomId: Int? = null,
|
||||||
): Response<DataContainer<ListContainer<Agent>>>
|
): Response<DataContainer<ListContainer<Agent>>>
|
||||||
|
|
||||||
@GET("outside/my/prompts")
|
@GET("outside/my/prompts")
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ import com.aiosman.ravenow.ui.gallery.OfficialGalleryScreen
|
|||||||
import com.aiosman.ravenow.ui.gallery.OfficialPhotographerScreen
|
import com.aiosman.ravenow.ui.gallery.OfficialPhotographerScreen
|
||||||
import com.aiosman.ravenow.ui.gallery.ProfileTimelineScreen
|
import com.aiosman.ravenow.ui.gallery.ProfileTimelineScreen
|
||||||
import com.aiosman.ravenow.ui.group.GroupChatInfoScreen
|
import com.aiosman.ravenow.ui.group.GroupChatInfoScreen
|
||||||
|
import com.aiosman.ravenow.ui.group.GroupMembersScreen
|
||||||
|
import com.aiosman.ravenow.ui.group.AddGroupMemberScreen
|
||||||
|
import com.aiosman.ravenow.ui.group.GroupProfileSettingsScreen
|
||||||
import com.aiosman.ravenow.ui.index.IndexScreen
|
import com.aiosman.ravenow.ui.index.IndexScreen
|
||||||
import com.aiosman.ravenow.ui.index.tabs.message.NotificationsScreen
|
import com.aiosman.ravenow.ui.index.tabs.message.NotificationsScreen
|
||||||
import com.aiosman.ravenow.ui.index.tabs.search.SearchScreen
|
import com.aiosman.ravenow.ui.index.tabs.search.SearchScreen
|
||||||
@@ -119,6 +122,9 @@ sealed class NavigationRoute(
|
|||||||
data object AddAgent : NavigationRoute("AddAgent")
|
data object AddAgent : NavigationRoute("AddAgent")
|
||||||
data object CreateGroupChat : NavigationRoute("CreateGroupChat")
|
data object CreateGroupChat : NavigationRoute("CreateGroupChat")
|
||||||
data object GroupInfo : NavigationRoute("GroupInfo/{id}")
|
data object GroupInfo : NavigationRoute("GroupInfo/{id}")
|
||||||
|
data object GroupMembers : NavigationRoute("GroupMembers/{id}")
|
||||||
|
data object AddGroupMember : NavigationRoute("AddGroupMember/{groupId}/{groupName}")
|
||||||
|
data object GroupProfileSettings : NavigationRoute("GroupProfileSettings/{id}")
|
||||||
data object VipSelPage : NavigationRoute("VipSelPage")
|
data object VipSelPage : NavigationRoute("VipSelPage")
|
||||||
data object RemoveAccountScreen: NavigationRoute("RemoveAccount")
|
data object RemoveAccountScreen: NavigationRoute("RemoveAccount")
|
||||||
data object NotificationScreen : NavigationRoute("NotificationScreen")
|
data object NotificationScreen : NavigationRoute("NotificationScreen")
|
||||||
@@ -613,6 +619,51 @@ fun NavigationController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = NavigationRoute.GroupMembers.route,
|
||||||
|
arguments = listOf(navArgument("id") { type = NavType.StringType })
|
||||||
|
) {
|
||||||
|
val encodedId = it.arguments?.getString("id")
|
||||||
|
val decodedId = encodedId?.let { java.net.URLDecoder.decode(it, "UTF-8") }
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalAnimatedContentScope provides this,
|
||||||
|
) {
|
||||||
|
GroupMembersScreen(decodedId ?: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = NavigationRoute.AddGroupMember.route,
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("groupId") { type = NavType.StringType },
|
||||||
|
navArgument("groupName") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val encodedGroupId = it.arguments?.getString("groupId")
|
||||||
|
val decodedGroupId = encodedGroupId?.let { java.net.URLDecoder.decode(it, "UTF-8") }
|
||||||
|
val encodedGroupName = it.arguments?.getString("groupName")
|
||||||
|
val decodedGroupName = encodedGroupName?.let { java.net.URLDecoder.decode(it, "UTF-8") }
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalAnimatedContentScope provides this,
|
||||||
|
) {
|
||||||
|
AddGroupMemberScreen(decodedGroupId ?: "", decodedGroupName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = NavigationRoute.GroupProfileSettings.route,
|
||||||
|
arguments = listOf(navArgument("id") { type = NavType.StringType })
|
||||||
|
) {
|
||||||
|
val encodedId = it.arguments?.getString("id")
|
||||||
|
val decodedId = encodedId?.let { java.net.URLDecoder.decode(it, "UTF-8") }
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalAnimatedContentScope provides this,
|
||||||
|
) {
|
||||||
|
GroupProfileSettingsScreen(decodedId ?: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
composable(route = NavigationRoute.NotificationScreen.route) {
|
composable(route = NavigationRoute.NotificationScreen.route) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalAnimatedContentScope provides this,
|
LocalAnimatedContentScope provides this,
|
||||||
@@ -701,6 +752,34 @@ fun NavHostController.navigateToGroupInfo(id: String) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun NavHostController.navigateToGroupMembers(id: String) {
|
||||||
|
val encodedId = java.net.URLEncoder.encode(id, "UTF-8")
|
||||||
|
|
||||||
|
navigate(
|
||||||
|
route = NavigationRoute.GroupMembers.route
|
||||||
|
.replace("{id}", encodedId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavHostController.navigateToAddGroupMember(groupId: String, groupName: String?) {
|
||||||
|
val encodedGroupId = java.net.URLEncoder.encode(groupId, "UTF-8")
|
||||||
|
val encodedGroupName = java.net.URLEncoder.encode(groupName ?: "", "UTF-8")
|
||||||
|
|
||||||
|
navigate(
|
||||||
|
route = NavigationRoute.AddGroupMember.route
|
||||||
|
.replace("{groupId}", encodedGroupId)
|
||||||
|
.replace("{groupName}", encodedGroupName)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavHostController.navigateToGroupProfileSettings(id: String) {
|
||||||
|
val encodedId = java.net.URLEncoder.encode(id, "UTF-8")
|
||||||
|
navigate(
|
||||||
|
route = NavigationRoute.GroupProfileSettings.route
|
||||||
|
.replace("{id}", encodedId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,586 @@
|
|||||||
|
package com.aiosman.ravenow.ui.group
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
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.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.ravenow.ui.composables.TabItem
|
||||||
|
import com.aiosman.ravenow.ui.composables.TabSpacer
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AddGroupMemberScreen(groupId: String, groupName: String?) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val systemUiController = rememberSystemUiController()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
var searchText by remember { mutableStateOf(TextFieldValue("")) }
|
||||||
|
var selectedMembers by remember { mutableStateOf(listOf<GroupMember>()) }
|
||||||
|
var selectedMemberIds by remember { mutableStateOf<Set<String>>(emptySet()) }
|
||||||
|
var pagerState = rememberPagerState(pageCount = { 2 })
|
||||||
|
var scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// LazyRow状态管理
|
||||||
|
val lazyRowState = rememberLazyListState()
|
||||||
|
|
||||||
|
// 清除错误信息
|
||||||
|
LaunchedEffect(searchText.text) {
|
||||||
|
if (AddGroupMemberViewModel.errorMessage != null) {
|
||||||
|
AddGroupMemberViewModel.clearError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听selectedMembers变化,当有新成员添加时自动滚动到最后一个
|
||||||
|
LaunchedEffect(selectedMembers.size) {
|
||||||
|
if (selectedMembers.isNotEmpty()) {
|
||||||
|
kotlinx.coroutines.delay(100)
|
||||||
|
lazyRowState.animateScrollToItem(selectedMembers.size - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
systemUiController.setNavigationBarColor(Color.Transparent)
|
||||||
|
AddGroupMemberViewModel.groupName = groupName
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(AppColors.background)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
|
|
||||||
|
// 头部
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 返回按钮
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
||||||
|
contentDescription = "back",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.group_chat_info_add_member),
|
||||||
|
fontSize = 17.sp,
|
||||||
|
fontWeight = FontWeight.W700,
|
||||||
|
color = AppColors.text,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索栏(暂时不实现,但保留UI)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
.background(
|
||||||
|
color = AppColors.inputBackground,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 13.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_nav_search),
|
||||||
|
contentDescription = stringResource(R.string.search),
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.secondaryText)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
BasicTextField(
|
||||||
|
value = searchText,
|
||||||
|
onValueChange = { searchText = it },
|
||||||
|
textStyle = androidx.compose.ui.text.TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 14.sp
|
||||||
|
),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
singleLine = true,
|
||||||
|
cursorBrush = SolidColor(AppColors.text),
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
Box {
|
||||||
|
if (searchText.text.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.search),
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已选成员列表
|
||||||
|
if (selectedMembers.isNotEmpty()) {
|
||||||
|
LazyRow(
|
||||||
|
state = lazyRowState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
items(selectedMembers) { member ->
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.width(48.dp)
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
CustomAsyncImage(
|
||||||
|
context = context,
|
||||||
|
imageUrl = member.avatar,
|
||||||
|
contentDescription = member.name,
|
||||||
|
defaultRes = R.drawable.default_avatar,
|
||||||
|
placeholderRes = R.drawable.default_avatar,
|
||||||
|
errorRes = R.drawable.default_avatar,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 删除按钮
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.background(AppColors.error, CircleShape)
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.noRippleClickable {
|
||||||
|
val (newSelectedMemberIds, newSelectedMembers) = AddGroupMemberViewModel.removeSelectedMember(
|
||||||
|
member, selectedMemberIds, selectedMembers
|
||||||
|
)
|
||||||
|
selectedMemberIds = newSelectedMemberIds
|
||||||
|
selectedMembers = newSelectedMembers
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "×",
|
||||||
|
color = AppColors.mainText,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 名称显示
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = if (member.name.length > 5) {
|
||||||
|
member.name.substring(0, 5) + "..."
|
||||||
|
} else {
|
||||||
|
member.name
|
||||||
|
},
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = AppColors.text,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab切换
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
verticalAlignment = Alignment.Bottom
|
||||||
|
) {
|
||||||
|
TabItem(
|
||||||
|
text = stringResource(R.string.chat_ai),
|
||||||
|
isSelected = pagerState.currentPage == 0,
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
pagerState.animateScrollToPage(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
TabSpacer()
|
||||||
|
TabItem(
|
||||||
|
text = stringResource(R.string.chat_friend),
|
||||||
|
isSelected = pagerState.currentPage == 1,
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
pagerState.animateScrollToPage(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容区域 - 自适应填满剩余高度
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
when (it) {
|
||||||
|
0 -> {
|
||||||
|
// AI智能体列表
|
||||||
|
AddMemberAiAgentListScreen(
|
||||||
|
searchText = searchText.text,
|
||||||
|
selectedMemberIds = selectedMemberIds,
|
||||||
|
excludeRoomId = null,
|
||||||
|
onMemberSelect = { member ->
|
||||||
|
val (newSelectedMemberIds, newSelectedMembers) = AddGroupMemberViewModel.toggleMemberSelection(
|
||||||
|
member, selectedMemberIds, selectedMembers
|
||||||
|
)
|
||||||
|
selectedMemberIds = newSelectedMemberIds
|
||||||
|
selectedMembers = newSelectedMembers
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
1 -> {
|
||||||
|
// 朋友列表
|
||||||
|
AddMemberFriendListScreen(
|
||||||
|
searchText = searchText.text,
|
||||||
|
selectedMemberIds = selectedMemberIds,
|
||||||
|
roomId = null,
|
||||||
|
onMemberSelect = { member ->
|
||||||
|
val (newSelectedMemberIds, newSelectedMembers) = AddGroupMemberViewModel.toggleMemberSelection(
|
||||||
|
member, selectedMemberIds, selectedMembers
|
||||||
|
)
|
||||||
|
selectedMemberIds = newSelectedMemberIds
|
||||||
|
selectedMembers = newSelectedMembers
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认按钮 - 固定在底部
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (selectedMembers.isNotEmpty()) {
|
||||||
|
scope.launch {
|
||||||
|
val success = AddGroupMemberViewModel.addMembersToGroup(selectedMembers)
|
||||||
|
if (success) {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = navigationBarPadding + 16.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = AppColors.main,
|
||||||
|
contentColor = AppColors.mainText,
|
||||||
|
disabledContainerColor = AppColors.disabledBackground,
|
||||||
|
disabledContentColor = AppColors.text
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(24.dp),
|
||||||
|
enabled = selectedMembers.isNotEmpty() && !AddGroupMemberViewModel.isLoading
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.lets_ride_upper),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 居中显示的错误提示弹窗
|
||||||
|
AddGroupMemberViewModel.errorMessage?.let { error ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(24.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.Center),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.8f),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = error,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
color = Color.Red,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支持excludeRoomId的AI智能体列表Screen
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AddMemberAiAgentListScreen(
|
||||||
|
searchText: String,
|
||||||
|
selectedMemberIds: Set<String> = emptySet(),
|
||||||
|
excludeRoomId: Int? = null,
|
||||||
|
onMemberSelect: (GroupMember) -> Unit
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
val viewModel = remember(excludeRoomId) { AddMemberAiAgentListViewModel(excludeRoomId) }
|
||||||
|
|
||||||
|
val filteredAgents = viewModel.getFilteredAgents(searchText)
|
||||||
|
|
||||||
|
val pullRefreshState = rememberPullRefreshState(
|
||||||
|
refreshing = viewModel.isRefreshing,
|
||||||
|
onRefresh = {
|
||||||
|
viewModel.refresh(searchText)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(listState) {
|
||||||
|
snapshotFlow { listState.layoutInfo.visibleItemsInfo }
|
||||||
|
.collect { visibleItems ->
|
||||||
|
if (visibleItems.isNotEmpty()) {
|
||||||
|
val lastVisibleItem = visibleItems.last()
|
||||||
|
if (lastVisibleItem.index >= filteredAgents.size - 3 && viewModel.hasMoreData && !viewModel.isLoadingMore && !viewModel.isLoading) {
|
||||||
|
viewModel.loadMore(searchText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.pullRefresh(pullRefreshState)
|
||||||
|
) {
|
||||||
|
if (viewModel.isLoading && filteredAgents.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "加载中...",
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = filteredAgents,
|
||||||
|
key = { it.id }
|
||||||
|
) { agent ->
|
||||||
|
MemberItem(
|
||||||
|
member = agent,
|
||||||
|
isSelected = selectedMemberIds.contains(agent.id),
|
||||||
|
onSelect = { onMemberSelect(agent) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.isLoadingMore) {
|
||||||
|
item {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "加载更多...",
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PullRefreshIndicator(
|
||||||
|
refreshing = viewModel.isRefreshing,
|
||||||
|
state = pullRefreshState,
|
||||||
|
modifier = Modifier.align(Alignment.TopCenter),
|
||||||
|
backgroundColor = AppColors.background,
|
||||||
|
contentColor = AppColors.main
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支持roomId的朋友列表Screen
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AddMemberFriendListScreen(
|
||||||
|
searchText: String,
|
||||||
|
selectedMemberIds: Set<String> = emptySet(),
|
||||||
|
roomId: Int? = null,
|
||||||
|
onMemberSelect: (GroupMember) -> Unit
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
val viewModel = remember(roomId) { AddMemberFriendListViewModel(roomId) }
|
||||||
|
|
||||||
|
val filteredFriends = viewModel.getFilteredFriends(searchText)
|
||||||
|
|
||||||
|
val pullRefreshState = rememberPullRefreshState(
|
||||||
|
refreshing = viewModel.isRefreshing,
|
||||||
|
onRefresh = {
|
||||||
|
viewModel.refresh(searchText)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(listState) {
|
||||||
|
snapshotFlow { listState.layoutInfo.visibleItemsInfo }
|
||||||
|
.collect { visibleItems ->
|
||||||
|
if (visibleItems.isNotEmpty()) {
|
||||||
|
val lastVisibleItem = visibleItems.last()
|
||||||
|
if (lastVisibleItem.index >= filteredFriends.size - 3 && viewModel.hasMoreData && !viewModel.isLoadingMore && !viewModel.isLoading) {
|
||||||
|
viewModel.loadMore(searchText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.pullRefresh(pullRefreshState)
|
||||||
|
) {
|
||||||
|
if (viewModel.isLoading && filteredFriends.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "加载中...",
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = filteredFriends,
|
||||||
|
key = { it.id }
|
||||||
|
) { friend ->
|
||||||
|
MemberItem(
|
||||||
|
member = friend,
|
||||||
|
isSelected = selectedMemberIds.contains(friend.id),
|
||||||
|
onSelect = { onMemberSelect(friend) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.isLoadingMore) {
|
||||||
|
item {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "加载更多...",
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PullRefreshIndicator(
|
||||||
|
refreshing = viewModel.isRefreshing,
|
||||||
|
state = pullRefreshState,
|
||||||
|
modifier = Modifier.align(Alignment.TopCenter),
|
||||||
|
backgroundColor = AppColors.background,
|
||||||
|
contentColor = AppColors.main
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
package com.aiosman.ravenow.ui.group
|
||||||
|
|
||||||
|
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.AppStore
|
||||||
|
import com.aiosman.ravenow.data.AccountService
|
||||||
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
|
import com.aiosman.ravenow.data.UserService
|
||||||
|
import com.aiosman.ravenow.data.UserServiceImpl
|
||||||
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
object AddGroupMemberViewModel : ViewModel() {
|
||||||
|
val accountService: AccountService = AccountServiceImpl()
|
||||||
|
val userService: UserService = UserServiceImpl()
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
var isLoading by mutableStateOf(false)
|
||||||
|
var errorMessage by mutableStateOf<String?>(null)
|
||||||
|
var groupName: String? = null
|
||||||
|
|
||||||
|
// 添加成员到群聊
|
||||||
|
suspend fun addMembersToGroup(
|
||||||
|
selectedMembers: List<GroupMember>
|
||||||
|
): Boolean {
|
||||||
|
return try {
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
if (groupName == null) {
|
||||||
|
isLoading = false
|
||||||
|
val errorMsg = "群聊名称不能为空"
|
||||||
|
showToast(errorMsg)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据isAi属性分别获取userIds和promptIds
|
||||||
|
val userIds = selectedMembers.filter { !it.isAi }.map { it.id }
|
||||||
|
val promptIds = selectedMembers.filter { it.isAi }.map { it.id }
|
||||||
|
|
||||||
|
// 使用创建群聊的API,传入群聊名称来添加成员到现有群聊
|
||||||
|
// 与创建群聊使用相同的方法,通过群聊名称标识目标群聊
|
||||||
|
val response = accountService.createGroupChat(groupName!!, userIds, promptIds, roomId = null)
|
||||||
|
if (response.isSuccessful && response.body() != null) {
|
||||||
|
isLoading = false
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
isLoading = false
|
||||||
|
val errorMsg = "添加成员失败: ${response.message()}"
|
||||||
|
showToast(errorMsg)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
isLoading = false
|
||||||
|
val errorMsg = "添加成员失败: ${e.message}"
|
||||||
|
showToast(errorMsg)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showToast(message: String) {
|
||||||
|
errorMessage = message
|
||||||
|
viewModelScope.launch {
|
||||||
|
kotlinx.coroutines.delay(3000)
|
||||||
|
errorMessage = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除错误信息
|
||||||
|
fun clearError() {
|
||||||
|
errorMessage = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加成员到选中列表
|
||||||
|
fun addSelectedMember(member: GroupMember, selectedMemberIds: Set<String>, selectedMembers: List<GroupMember>): Pair<Set<String>, List<GroupMember>> {
|
||||||
|
val newSelectedMemberIds = selectedMemberIds + member.id
|
||||||
|
val newSelectedMembers = if (selectedMembers.none { it.id == member.id }) {
|
||||||
|
selectedMembers + member
|
||||||
|
} else {
|
||||||
|
selectedMembers
|
||||||
|
}
|
||||||
|
return Pair(newSelectedMemberIds, newSelectedMembers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从选中列表移除成员
|
||||||
|
fun removeSelectedMember(member: GroupMember, selectedMemberIds: Set<String>, selectedMembers: List<GroupMember>): Pair<Set<String>, List<GroupMember>> {
|
||||||
|
val newSelectedMemberIds = selectedMemberIds - member.id
|
||||||
|
val newSelectedMembers = selectedMembers.filter { it.id != member.id }
|
||||||
|
return Pair(newSelectedMemberIds, newSelectedMembers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换成员选中状态
|
||||||
|
fun toggleMemberSelection(member: GroupMember, selectedMemberIds: Set<String>, selectedMembers: List<GroupMember>): Pair<Set<String>, List<GroupMember>> {
|
||||||
|
return if (selectedMemberIds.contains(member.id)) {
|
||||||
|
removeSelectedMember(member, selectedMemberIds, selectedMembers)
|
||||||
|
} else {
|
||||||
|
addSelectedMember(member, selectedMemberIds, selectedMembers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支持roomId的AI智能体列表ViewModel
|
||||||
|
class AddMemberAiAgentListViewModel(private val excludeRoomId: Int? = null) : ViewModel() {
|
||||||
|
private val accountService: AccountService = AccountServiceImpl()
|
||||||
|
|
||||||
|
var aiAgents by mutableStateOf<List<GroupMember>>(emptyList())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var isLoading by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var isRefreshing by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var isLoadingMore by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var currentPage by mutableStateOf(1)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var hasMoreData by mutableStateOf(true)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var errorMessage by mutableStateOf<String?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
private val pageSize = 20
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadAgents(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadAgents(page: Int, isRefresh: Boolean = false, searchText: String = "") {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
if (isRefresh) {
|
||||||
|
isRefreshing = true
|
||||||
|
} else if (page == 1) {
|
||||||
|
isLoading = true
|
||||||
|
} else {
|
||||||
|
isLoadingMore = true
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage = null
|
||||||
|
|
||||||
|
val response = accountService.getAgent(page, pageSize, excludeRoomId = excludeRoomId, title = if (searchText.isNotEmpty()) searchText else null, desc = if (searchText.isNotEmpty()) searchText else null)
|
||||||
|
if (response.isSuccessful && response.body() != null) {
|
||||||
|
val agentData = response.body()!!
|
||||||
|
val newAgents: List<GroupMember> = agentData.data.list.map { agent ->
|
||||||
|
GroupMember(
|
||||||
|
id = agent.openId,
|
||||||
|
name = agent.title,
|
||||||
|
avatar = "${ApiClient.BASE_API_URL+"/outside"}${agent.avatar}"+"?token="+"${AppStore.token}",
|
||||||
|
isAi = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRefresh || page == 1) {
|
||||||
|
aiAgents = newAgents
|
||||||
|
currentPage = 1
|
||||||
|
} else {
|
||||||
|
aiAgents = aiAgents + newAgents
|
||||||
|
currentPage = page
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMoreData = newAgents.size >= pageSize
|
||||||
|
} else {
|
||||||
|
errorMessage = "获取AI智能体列表失败: ${response.message()}"
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errorMessage = "获取AI智能体列表失败: ${e.message}"
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
|
isRefreshing = false
|
||||||
|
isLoadingMore = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh(searchText: String = "") {
|
||||||
|
loadAgents(1, true, searchText)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadMore(searchText: String = "") {
|
||||||
|
if (hasMoreData && !isLoadingMore && !isLoading) {
|
||||||
|
loadAgents(currentPage + 1, false, searchText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearError() {
|
||||||
|
errorMessage = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFilteredAgents(searchText: String): List<GroupMember> {
|
||||||
|
return if (searchText.isEmpty()) {
|
||||||
|
aiAgents
|
||||||
|
} else {
|
||||||
|
aiAgents.filter { it.name.contains(searchText, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支持roomId的朋友列表ViewModel
|
||||||
|
class AddMemberFriendListViewModel(private val roomId: Int? = null) : ViewModel() {
|
||||||
|
private val userService: UserService = UserServiceImpl()
|
||||||
|
|
||||||
|
var friends by mutableStateOf<List<GroupMember>>(emptyList())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var isLoading by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var isRefreshing by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var isLoadingMore by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var currentPage by mutableStateOf(1)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var hasMoreData by mutableStateOf(true)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var errorMessage by mutableStateOf<String?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
private val pageSize = 20
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadFriends(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadFriends(page: Int, isRefresh: Boolean = false, searchText: String = "") {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
if (isRefresh) {
|
||||||
|
isRefreshing = true
|
||||||
|
} else if (page == 1) {
|
||||||
|
isLoading = true
|
||||||
|
} else {
|
||||||
|
isLoadingMore = true
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage = null
|
||||||
|
|
||||||
|
val userData = userService.getUsers(pageSize, page, nickname = if (searchText.isNotEmpty()) searchText else null, roomId = roomId)
|
||||||
|
val newFriends: List<GroupMember> = userData.list.map { user ->
|
||||||
|
GroupMember(
|
||||||
|
id = user.chatAIId,
|
||||||
|
name = user.nickName,
|
||||||
|
avatar = user.avatar,
|
||||||
|
isAi = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRefresh || page == 1) {
|
||||||
|
friends = newFriends
|
||||||
|
currentPage = 1
|
||||||
|
} else {
|
||||||
|
friends = friends + newFriends
|
||||||
|
currentPage = page
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMoreData = newFriends.size >= pageSize
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errorMessage = "获取朋友列表失败: ${e.message}"
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
|
isRefreshing = false
|
||||||
|
isLoadingMore = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh(searchText: String = "") {
|
||||||
|
loadFriends(1, true, searchText)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadMore(searchText: String = "") {
|
||||||
|
if (hasMoreData && !isLoadingMore && !isLoading) {
|
||||||
|
loadFriends(currentPage + 1, false, searchText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearError() {
|
||||||
|
errorMessage = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFilteredFriends(searchText: String): List<GroupMember> {
|
||||||
|
return if (searchText.isEmpty()) {
|
||||||
|
friends
|
||||||
|
} else {
|
||||||
|
friends.filter { it.name.contains(searchText, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -43,6 +43,8 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
|||||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
import com.aiosman.ravenow.ui.index.NavItem
|
import com.aiosman.ravenow.ui.index.NavItem
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import com.aiosman.ravenow.ui.navigateToGroupMembers
|
||||||
|
import com.aiosman.ravenow.ui.navigateToGroupProfileSettings
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -395,14 +397,14 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(13.dp))
|
Spacer(modifier = Modifier.height(13.dp))
|
||||||
|
|
||||||
// 设置聊天主题
|
// 群资料设置
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.padding(12.dp)
|
.padding(12.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
// TODO: 实现设置聊天主题功能
|
navController.navigateToGroupProfileSettings(groupId)
|
||||||
},
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
@@ -415,7 +417,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.group_chat_info_group_settings),
|
text = "群资料设置",
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontSize = 15.sp
|
fontSize = 15.sp
|
||||||
@@ -476,7 +478,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.padding(12.dp)
|
.padding(12.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
// 静态占位
|
navController.navigateToGroupMembers(groupId)
|
||||||
},
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ import com.aiosman.ravenow.data.api.AgentRule
|
|||||||
import com.aiosman.ravenow.data.api.AgentRuleQuota
|
import com.aiosman.ravenow.data.api.AgentRuleQuota
|
||||||
import com.aiosman.ravenow.data.api.CreateAgentRuleRequestBody
|
import com.aiosman.ravenow.data.api.CreateAgentRuleRequestBody
|
||||||
import com.aiosman.ravenow.data.api.UpdateAgentRuleRequestBody
|
import com.aiosman.ravenow.data.api.UpdateAgentRuleRequestBody
|
||||||
import com.aiosman.ravenow.data.api.CreateAgentRuleRequestBody
|
|
||||||
import com.aiosman.ravenow.data.api.AgentRule
|
|
||||||
import com.aiosman.ravenow.data.api.AgentRuleQuota
|
|
||||||
import com.aiosman.ravenow.data.parseErrorResponse
|
import com.aiosman.ravenow.data.parseErrorResponse
|
||||||
import com.aiosman.ravenow.entity.ChatNotification
|
import com.aiosman.ravenow.entity.ChatNotification
|
||||||
import com.aiosman.ravenow.entity.GroupInfo
|
import com.aiosman.ravenow.entity.GroupInfo
|
||||||
|
|||||||
@@ -0,0 +1,431 @@
|
|||||||
|
package com.aiosman.ravenow.ui.group
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
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.draw.rotate
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.layout.positionInRoot
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
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
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.entity.GroupMember
|
||||||
|
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.navigateToAddGroupMember
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupMembersScreen(groupId: String) {
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
val viewModel = viewModel<GroupMembersViewModel>(
|
||||||
|
key = "GroupMembersViewModel_$groupId",
|
||||||
|
factory = object : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return GroupMembersViewModel(groupId) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var selectedMemberPosition by remember { mutableStateOf<Pair<Offset, Float>?>(null) }
|
||||||
|
var selectedMemberId by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(AppColors.background)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
// 顶部导航栏
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(AppColors.background)
|
||||||
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 12.dp, horizontal = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
androidx.compose.foundation.Image(
|
||||||
|
painter = painterResource(R.drawable.rider_pro_back_icon),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateUp()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.group_members_title),
|
||||||
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 17.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
androidx.compose.foundation.Image(
|
||||||
|
painter = painterResource(R.drawable.rider_pro_add_other),
|
||||||
|
contentDescription = stringResource(R.string.group_chat_info_add_member),
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容区域
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(0.dp)
|
||||||
|
) {
|
||||||
|
// 当前用户信息
|
||||||
|
item {
|
||||||
|
if (viewModel.currentUserMember != null) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.group_members_you),
|
||||||
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
|
color = AppColors.text.copy(alpha = 0.6f),
|
||||||
|
fontSize = 13.sp
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(start = 12.dp, bottom = 8.dp)
|
||||||
|
)
|
||||||
|
CurrentUserItem(
|
||||||
|
member = viewModel.currentUserMember!!,
|
||||||
|
isAdmin = viewModel.groupInfo?.isCreator == true
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 群成员列表
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
R.string.group_members_list,
|
||||||
|
viewModel.members.size + (if (viewModel.currentUserMember != null) 1 else 0)
|
||||||
|
),
|
||||||
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
|
color = AppColors.text.copy(alpha = 0.6f),
|
||||||
|
fontSize = 13.sp
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items(viewModel.members) { member ->
|
||||||
|
MemberItem(
|
||||||
|
member = member,
|
||||||
|
isAdmin = viewModel.groupInfo?.isCreator == true,
|
||||||
|
onSendMessage = {
|
||||||
|
// TODO: 实现发消息功能
|
||||||
|
},
|
||||||
|
onMenuClick = { position, height ->
|
||||||
|
if (selectedMemberId == member.userId) {
|
||||||
|
selectedMemberId = null
|
||||||
|
selectedMemberPosition = null
|
||||||
|
} else {
|
||||||
|
selectedMemberId = member.userId
|
||||||
|
selectedMemberPosition = Pair(position, height)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDeleteMember = {
|
||||||
|
viewModel.deleteMember(member.userId)
|
||||||
|
selectedMemberId = null
|
||||||
|
selectedMemberPosition = null
|
||||||
|
},
|
||||||
|
isMenuVisible = selectedMemberId == member.userId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹窗 - 显示在最上层
|
||||||
|
selectedMemberPosition?.let { (position, height) ->
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val screenWidth = with(density) { configuration.screenWidthDp.dp }
|
||||||
|
val horizontalOffset = (screenWidth - 238.dp) / 2
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(238.dp)
|
||||||
|
.height(60.dp)
|
||||||
|
.zIndex(1000f)
|
||||||
|
.offset {
|
||||||
|
val xOffset = with(density) { horizontalOffset.toPx().toInt() }
|
||||||
|
val yOffset = (position.y + height + with(density) { 4.dp.toPx() }).toInt()
|
||||||
|
IntOffset(x = xOffset, y = yOffset)
|
||||||
|
}
|
||||||
|
.shadow(
|
||||||
|
elevation = 8.dp,
|
||||||
|
shape = RoundedCornerShape(24.dp),
|
||||||
|
spotColor = Color.Black.copy(alpha = 0.15f),
|
||||||
|
ambientColor = Color.Black.copy(alpha = 0.08f)
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(24.dp))
|
||||||
|
.background(
|
||||||
|
brush = androidx.compose.ui.graphics.Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFF262626).copy(alpha = 0.4f),
|
||||||
|
Color(0xFFF5F5F5).copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.clickable {
|
||||||
|
selectedMemberId?.let { memberId ->
|
||||||
|
viewModel.deleteMember(memberId)
|
||||||
|
selectedMemberId = null
|
||||||
|
selectedMemberPosition = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.group_members_delete_member),
|
||||||
|
color = Color(0xFFFF3B30),
|
||||||
|
fontSize = 15.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CurrentUserItem(
|
||||||
|
member: GroupMember,
|
||||||
|
isAdmin: Boolean
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 头像
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
if (member.avatar.isNotEmpty()) Color.Transparent
|
||||||
|
else AppColors.decentBackground
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (member.avatar.isNotEmpty()) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = member.avatar,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(CircleShape),
|
||||||
|
contentDescription = member.nickname
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 默认头像占位
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF0EEF1)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.default_avatar),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = AppColors.text.copy(alpha = 0.5f),
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
|
// 名称和身份
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = member.nickname,
|
||||||
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (isAdmin) {
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.group_members_admin),
|
||||||
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MemberItem(
|
||||||
|
member: GroupMember,
|
||||||
|
isAdmin: Boolean,
|
||||||
|
onSendMessage: () -> Unit,
|
||||||
|
onMenuClick: (Offset, Float) -> Unit,
|
||||||
|
onDeleteMember: () -> Unit,
|
||||||
|
isMenuVisible: Boolean
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val density = LocalDensity.current
|
||||||
|
var itemPosition by remember { mutableStateOf(Offset.Zero) }
|
||||||
|
var itemHeight by remember { mutableStateOf(0f) }
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
itemPosition = coordinates.positionInRoot()
|
||||||
|
itemHeight = coordinates.size.height.toFloat()
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 头像
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
if (member.avatar.isNotEmpty()) Color.Transparent
|
||||||
|
else AppColors.decentBackground
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (member.avatar.isNotEmpty()) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = member.avatar,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(CircleShape),
|
||||||
|
contentDescription = member.nickname
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 默认头像占位
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF0EEF1)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.mipmap.group_copy),
|
||||||
|
contentDescription = "默认头像",
|
||||||
|
modifier = Modifier.size(40.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
|
// 名称
|
||||||
|
Text(
|
||||||
|
text = member.nickname,
|
||||||
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
// 菜单按钮
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = { onMenuClick(itemPosition, itemHeight) },
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
) {
|
||||||
|
androidx.compose.foundation.Image(
|
||||||
|
painter = painterResource(R.drawable.rider_pro_more_horizon),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
|
||||||
|
// 发消息按钮 - 右对齐到与Switch相同的位置
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(14.dp))
|
||||||
|
.background(AppColors.decentBackground)
|
||||||
|
.clickable { onSendMessage() }
|
||||||
|
.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.group_members_send_message),
|
||||||
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.aiosman.ravenow.ui.group
|
||||||
|
|
||||||
|
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.AppStore
|
||||||
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import com.aiosman.ravenow.entity.GroupInfo
|
||||||
|
import com.aiosman.ravenow.entity.GroupMember
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class GroupMembersViewModel(
|
||||||
|
private val groupId: String
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
var groupInfo by mutableStateOf<GroupInfo?>(null)
|
||||||
|
var currentUserMember by mutableStateOf<GroupMember?>(null)
|
||||||
|
var members by mutableStateOf<List<GroupMember>>(emptyList())
|
||||||
|
var isLoading by mutableStateOf(false)
|
||||||
|
var error by mutableStateOf<String?>(null)
|
||||||
|
var requireApproval by mutableStateOf(true) // 默认开启
|
||||||
|
var roomId by mutableStateOf<Int?>(null)
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadGroupMembers()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadGroupMembers() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
isLoading = true
|
||||||
|
error = null
|
||||||
|
|
||||||
|
// 调用接口获取群聊详情
|
||||||
|
val response = ApiClient.api.getRoomDetail(trtcId = groupId)
|
||||||
|
|
||||||
|
if (response.isSuccessful && response.body() != null) {
|
||||||
|
val room = response.body()!!.data
|
||||||
|
|
||||||
|
// 保存roomId
|
||||||
|
roomId = room.id
|
||||||
|
|
||||||
|
// 设置群信息
|
||||||
|
groupInfo = GroupInfo(
|
||||||
|
groupId = groupId,
|
||||||
|
groupName = room.name,
|
||||||
|
groupAvatar = if (room.avatar.isNullOrEmpty()) {
|
||||||
|
val groupIdBase64 = android.util.Base64.encodeToString(
|
||||||
|
groupId.toByteArray(),
|
||||||
|
android.util.Base64.NO_WRAP
|
||||||
|
)
|
||||||
|
"${ApiClient.RETROFIT_URL}group/avatar?groupIdBase64=$groupIdBase64&token=${AppStore.token}"
|
||||||
|
} else {
|
||||||
|
"${ApiClient.BASE_API_URL}/outside${room.avatar}?token=${AppStore.token}"
|
||||||
|
},
|
||||||
|
memberCount = room.userCount,
|
||||||
|
isCreator = room.creator.userId == MyProfileViewModel.profile?.id.toString()
|
||||||
|
)
|
||||||
|
|
||||||
|
// 获取当前用户ID
|
||||||
|
val currentUserId = MyProfileViewModel.profile?.id.toString()
|
||||||
|
|
||||||
|
// 转换成员列表
|
||||||
|
val allMembers = room.users.map { user ->
|
||||||
|
val avatarUrl = if (user.profile.avatar.isNullOrEmpty()) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
// 与动态列表和关注列表一致,使用 BASE_SERVER 构建头像URL,不需要token
|
||||||
|
"${ApiClient.BASE_SERVER}${user.profile.avatar}"
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupMember(
|
||||||
|
userId = user.userId,
|
||||||
|
nickname = user.profile.nickname.ifEmpty { user.profile.username },
|
||||||
|
avatar = avatarUrl,
|
||||||
|
isOwner = user.userId == room.creator.userId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分离当前用户和其他成员
|
||||||
|
currentUserMember = allMembers.find { it.userId == currentUserId }
|
||||||
|
members = allMembers.filter { it.userId != currentUserId }
|
||||||
|
|
||||||
|
} else {
|
||||||
|
error = "获取群成员列表失败"
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error = e.message ?: "加载失败"
|
||||||
|
Log.e("GroupMembersViewModel", "加载群成员失败", e)
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
loadGroupMembers()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteMember(memberId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
// TODO: 实现删除成员的API调用
|
||||||
|
// 删除成功后刷新列表
|
||||||
|
loadGroupMembers()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error = e.message ?: "删除成员失败"
|
||||||
|
Log.e("GroupMembersViewModel", "删除成员失败", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleRequireApproval() {
|
||||||
|
requireApproval = !requireApproval
|
||||||
|
// TODO: 实现更新设置的API调用
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
package com.aiosman.ravenow.ui.group
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.InputStream
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
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.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
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
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupProfileSettingsScreen(groupId: String) {
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
val viewModel = viewModel<GroupProfileSettingsViewModel>(
|
||||||
|
key = "GroupProfileSettingsViewModel_$groupId",
|
||||||
|
factory = object : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return GroupProfileSettingsViewModel(groupId) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 群图标选择器
|
||||||
|
val groupIconPicker = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.GetContent()
|
||||||
|
) { uri ->
|
||||||
|
uri?.let {
|
||||||
|
// 直接转换为 Bitmap 并显示,不进行裁剪
|
||||||
|
kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val bitmap = uriToBitmap(context, it)
|
||||||
|
bitmap?.let { bmp ->
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
viewModel.setGroupIcon(bmp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 群头像选择器
|
||||||
|
val groupAvatarPicker = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.GetContent()
|
||||||
|
) { uri ->
|
||||||
|
uri?.let {
|
||||||
|
// 直接转换为 Bitmap 并显示,不进行裁剪
|
||||||
|
kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val bitmap = uriToBitmap(context, it)
|
||||||
|
bitmap?.let { bmp ->
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
viewModel.setGroupAvatar(bmp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(AppColors.background)
|
||||||
|
) {
|
||||||
|
// 群默认图标图片区域(渐变背景)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(249.dp)
|
||||||
|
.background(
|
||||||
|
brush = Brush.horizontalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFF7C45ED),
|
||||||
|
Color(0xFFE91E63)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(bottomStart = 20.dp, bottomEnd = 20.dp)
|
||||||
|
)
|
||||||
|
.noRippleClickable {
|
||||||
|
groupIconPicker.launch("image/*")
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// 显示选中的图标或默认渐变
|
||||||
|
viewModel.groupIconBitmap?.let { bitmap ->
|
||||||
|
Image(
|
||||||
|
bitmap = bitmap.asImageBitmap(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 顶部导航栏(显示在渐变背景上层)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.zIndex(1f)
|
||||||
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 12.dp, horizontal = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.rider_pro_back_icon),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateUp()
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(Color.White)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.group_info_edit),
|
||||||
|
style = TextStyle(
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 群头像区域
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 270.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(70.dp)
|
||||||
|
) {
|
||||||
|
// 群头像显示
|
||||||
|
if (viewModel.groupInfo?.groupAvatar?.isNotEmpty() == true) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = viewModel.groupInfo!!.groupAvatar,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(RoundedCornerShape(12.dp)),
|
||||||
|
contentDescription = "群聊头像"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(AppColors.decentBackground),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = viewModel.groupInfo?.groupName?.firstOrNull()?.toString() ?: "",
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 32.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右下角加号按钮
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.size(22.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
brush = Brush.horizontalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFF7C45ED),
|
||||||
|
Color(0xFF7BD8F8)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.noRippleClickable {
|
||||||
|
groupAvatarPicker.launch("image/*")
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "+",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// 群聊名称编辑框
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(AppColors.decentBackground)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.group_name)+":",
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text.copy(alpha = 0.7f),
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
BasicTextField(
|
||||||
|
value = viewModel.groupName,
|
||||||
|
onValueChange = { viewModel.updateGroupName(it) },
|
||||||
|
textStyle = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 15.sp
|
||||||
|
),
|
||||||
|
cursorBrush = SolidColor(AppColors.text),
|
||||||
|
modifier = Modifier.padding(start = 70.dp),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
// 保存按钮
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 24.dp)
|
||||||
|
.height(50.dp)
|
||||||
|
.clip(RoundedCornerShape(25.dp))
|
||||||
|
.background(
|
||||||
|
brush = Brush.horizontalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFF7C45ED),
|
||||||
|
Color(0xFF7BD8F8)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.noRippleClickable {
|
||||||
|
// TODO: 实现保存功能
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.save),
|
||||||
|
style = TextStyle(
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 17.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将 Uri 转换为 Bitmap 的辅助函数
|
||||||
|
fun uriToBitmap(context: android.content.Context, uri: Uri): Bitmap? {
|
||||||
|
return try {
|
||||||
|
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
|
||||||
|
BitmapFactory.decodeStream(inputStream)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.aiosman.ravenow.ui.group
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
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.entity.GroupInfo
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class GroupProfileSettingsViewModel(
|
||||||
|
private val groupId: String
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
var groupInfo by mutableStateOf<GroupInfo?>(null)
|
||||||
|
var groupName by mutableStateOf("")
|
||||||
|
var groupIconBitmap by mutableStateOf<Bitmap?>(null)
|
||||||
|
var groupAvatarBitmap by mutableStateOf<Bitmap?>(null)
|
||||||
|
var isLoading by mutableStateOf(false)
|
||||||
|
var error by mutableStateOf<String?>(null)
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadGroupInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadGroupInfo() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
isLoading = true
|
||||||
|
error = null
|
||||||
|
// 直接调用 API 加载群信息
|
||||||
|
val response = com.aiosman.ravenow.data.api.ApiClient.api.getRoomDetail(trtcId = groupId)
|
||||||
|
val room = response.body()?.data
|
||||||
|
groupInfo = room?.let {
|
||||||
|
com.aiosman.ravenow.entity.GroupInfo(
|
||||||
|
groupId = groupId,
|
||||||
|
groupName = it.name,
|
||||||
|
groupAvatar = if (it.avatar.isNullOrEmpty()) {
|
||||||
|
val groupIdBase64 = android.util.Base64.encodeToString(
|
||||||
|
groupId.toByteArray(),
|
||||||
|
android.util.Base64.NO_WRAP
|
||||||
|
)
|
||||||
|
"${com.aiosman.ravenow.data.api.ApiClient.RETROFIT_URL}group/avatar?groupIdBase64=$groupIdBase64&token=${com.aiosman.ravenow.AppStore.token}"
|
||||||
|
} else {
|
||||||
|
"${com.aiosman.ravenow.data.api.ApiClient.BASE_API_URL}/outside${it.avatar}?token=${com.aiosman.ravenow.AppStore.token}"
|
||||||
|
},
|
||||||
|
memberCount = room.userCount,
|
||||||
|
isCreator = room.creator.userId == com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel.profile?.id.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
groupInfo?.let {
|
||||||
|
groupName = it.groupName
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error = e.message ?: "加载失败"
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateGroupName(newName: String) {
|
||||||
|
groupName = newName
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setGroupIcon(bitmap: Bitmap) {
|
||||||
|
groupIconBitmap = bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setGroupAvatar(bitmap: Bitmap) {
|
||||||
|
groupAvatarBitmap = bitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -14,20 +14,19 @@
|
|||||||
<string name="followers_upper">粉丝</string>
|
<string name="followers_upper">粉丝</string>
|
||||||
<string name="favourites_upper">收藏</string>
|
<string name="favourites_upper">收藏</string>
|
||||||
<string name="notifications_upper">消息</string>
|
<string name="notifications_upper">消息</string>
|
||||||
<string name="following_upper">关注</string>
|
<string name="following_upper">关注11</string>
|
||||||
<string name="unfollow_upper">取消关注</string>
|
<string name="unfollow_upper">取消关注</string>
|
||||||
<string name="comment_count">%d条评论</string>
|
<string name="comment_count">%d条评论</string>
|
||||||
<string name="post_comment_hint">快来互动吧...</string>
|
<string name="post_comment_hint">快来互动吧...</string>
|
||||||
<string name="follow_upper">关注</string>
|
<string name="follow_upper">关注3</string>
|
||||||
<string name="follow_upper_had">已关注</string>
|
<string name="follow_upper_had">已关注</string>
|
||||||
<string name="login_upper">登录</string>
|
<string name="login_upper">登录</string>
|
||||||
<string name="lets_ride_upper">确认</string>
|
<string name="lets_ride_upper">确认</string>
|
||||||
<string name="or_login_with">其他账号登录</string>
|
<string name="or_login_with">其他账号登录</string>
|
||||||
<string name="remember_me">记住登录</string>
|
<string name="remember_me">记住我</string>
|
||||||
<string name="forgot_password">忘记密码</string>
|
<string name="forgot_password">忘记密码</string>
|
||||||
<string name="login_password_label">密码</string>
|
<string name="login_password_label">密码</string>
|
||||||
<string name="login_email_label">邮箱</string>
|
<string name="login_email_label">邮箱</string>
|
||||||
<string name="confirm_password_label">确认密码</string>
|
|
||||||
<string name="text_error_email_required">邮箱是必填项</string>
|
<string name="text_error_email_required">邮箱是必填项</string>
|
||||||
<string name="text_error_password_required">密码是必填项</string>
|
<string name="text_error_password_required">密码是必填项</string>
|
||||||
<string name="text_hint_email">输入邮箱</string>
|
<string name="text_hint_email">输入邮箱</string>
|
||||||
@@ -78,7 +77,7 @@
|
|||||||
<string name="download">下载</string>
|
<string name="download">下载</string>
|
||||||
<string name="original">原始图片</string>
|
<string name="original">原始图片</string>
|
||||||
<string name="favourites">收藏</string>
|
<string name="favourites">收藏</string>
|
||||||
<string name="dark_mode">暗色模式</string>
|
<string name="dark_mode">暗黑模式</string>
|
||||||
<string name="light_mode">明亮模式</string>
|
<string name="light_mode">明亮模式</string>
|
||||||
<string name="update_find_new_version">发现新版本</string>
|
<string name="update_find_new_version">发现新版本</string>
|
||||||
<string name="update_update_now">立即更新</string>
|
<string name="update_update_now">立即更新</string>
|
||||||
@@ -95,7 +94,7 @@
|
|||||||
<string name="reset_mail_send_failed">邮件发送失败,请检查您的网络连接或稍后重试。</string>
|
<string name="reset_mail_send_failed">邮件发送失败,请检查您的网络连接或稍后重试。</string>
|
||||||
<string name="seconds_ago">%1d秒前</string>
|
<string name="seconds_ago">%1d秒前</string>
|
||||||
<string name="minutes_ago">%1d分钟前</string>
|
<string name="minutes_ago">%1d分钟前</string>
|
||||||
<string name="private_policy_template">我同意派派的</string>
|
<string name="private_policy_template">同意</string>
|
||||||
<string name="private_policy_keyword">用户协议</string>
|
<string name="private_policy_keyword">用户协议</string>
|
||||||
<string name="gallery">图片</string>
|
<string name="gallery">图片</string>
|
||||||
<string name="chat_upper">私信</string>
|
<string name="chat_upper">私信</string>
|
||||||
@@ -119,9 +118,8 @@
|
|||||||
<string name="close">关闭</string>
|
<string name="close">关闭</string>
|
||||||
<string name="about_rave_now">关于Rave Now</string>
|
<string name="about_rave_now">关于Rave Now</string>
|
||||||
<string name="blocked">已拉黑</string>
|
<string name="blocked">已拉黑</string>
|
||||||
<string name="blocked_users">被屏蔽的用户</string>
|
|
||||||
<string name="feedback">反馈</string>
|
<string name="feedback">反馈</string>
|
||||||
<string name="account_and_security">账号安全</string>
|
<string name="account_and_security">账户与安全</string>
|
||||||
<string name="remove_account">删除账户</string>
|
<string name="remove_account">删除账户</string>
|
||||||
<string name="remove_account_desc">注销账号为不可逆的操作,请确认</string>
|
<string name="remove_account_desc">注销账号为不可逆的操作,请确认</string>
|
||||||
<string name="remove_account_password_hint">输入密码以确认</string>
|
<string name="remove_account_password_hint">输入密码以确认</string>
|
||||||
@@ -145,8 +143,8 @@
|
|||||||
<string name="agent_desc_hint">示例: 一位经验丰富的销售员,擅长通过幽默风趣的语言和生动的案例,将复杂的产品转化为客户易于理解并感兴趣的话题</string>
|
<string name="agent_desc_hint">示例: 一位经验丰富的销售员,擅长通过幽默风趣的语言和生动的案例,将复杂的产品转化为客户易于理解并感兴趣的话题</string>
|
||||||
<string name="agent_create">创建智能体</string>
|
<string name="agent_create">创建智能体</string>
|
||||||
<string name="create_confirm">好的,就它了</string>
|
<string name="create_confirm">好的,就它了</string>
|
||||||
<string name="moment_content_hint">你的帖子需要一些灵感吗?让AI帮助你!</string>
|
<string name="moment_content_hint">需要一些灵感来写文章吗?让人工智能来帮你!</string>
|
||||||
<string name="moment_ai_co">文案优化</string>
|
<string name="moment_ai_co">AI文案优化</string>
|
||||||
<string name="moment_ai_delete">删除</string>
|
<string name="moment_ai_delete">删除</string>
|
||||||
<string name="moment_ai_apply">应用</string>
|
<string name="moment_ai_apply">应用</string>
|
||||||
<string name="chat_ai">智能体</string>
|
<string name="chat_ai">智能体</string>
|
||||||
@@ -289,12 +287,14 @@
|
|||||||
<string name="group_chat_info_done">完成</string>
|
<string name="group_chat_info_done">完成</string>
|
||||||
<string name="group_chat_info_recharge_hint">可通过充值获得更多派币</string>
|
<string name="group_chat_info_recharge_hint">可通过充值获得更多派币</string>
|
||||||
|
|
||||||
<!-- Edit Profile Extras -->
|
<!-- 群聊成员 -->
|
||||||
<string name="mbti_type">MBTI 类型</string>
|
<string name="group_members_title">群聊成员</string>
|
||||||
<string name="zodiac">星座</string>
|
<string name="group_members_require_approval">须批准才能加入</string>
|
||||||
<string name="save">保存</string>
|
<string name="group_members_you">你</string>
|
||||||
<string name="choose_mbti">选择 MBTI</string>
|
<string name="group_members_admin">管理员</string>
|
||||||
<string name="choose_zodiac">选择星座</string>
|
<string name="group_members_list">群成员(%d)</string>
|
||||||
|
<string name="group_members_send_message">发消息</string>
|
||||||
|
<string name="group_members_delete_member">Delete Member</string>
|
||||||
|
|
||||||
<!-- Side Menu -->
|
<!-- Side Menu -->
|
||||||
<string name="scan_qr">扫一扫</string>
|
<string name="scan_qr">扫一扫</string>
|
||||||
@@ -303,4 +303,7 @@
|
|||||||
<string name="follow_system">跟随系统</string>
|
<string name="follow_system">跟随系统</string>
|
||||||
<string name="message_notification">消息通知</string>
|
<string name="message_notification">消息通知</string>
|
||||||
<string name="logout_confirm">退出登录</string>
|
<string name="logout_confirm">退出登录</string>
|
||||||
|
|
||||||
|
<string name="blocked_users">被屏蔽的用户</string>
|
||||||
|
<string name="confirm_password_label">确认密码</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -23,11 +23,10 @@
|
|||||||
<string name="login_upper">Log in</string>
|
<string name="login_upper">Log in</string>
|
||||||
<string name="lets_ride_upper">Let\'s Rave Now</string>
|
<string name="lets_ride_upper">Let\'s Rave Now</string>
|
||||||
<string name="or_login_with">or</string>
|
<string name="or_login_with">or</string>
|
||||||
<string name="remember_me">Remember login</string>
|
<string name="remember_me">Remember me.</string>
|
||||||
<string name="forgot_password">Forgot password?</string>
|
<string name="forgot_password">Forgot password?</string>
|
||||||
<string name="login_password_label">What\'s your password</string>
|
<string name="login_password_label">What\'s your password</string>
|
||||||
<string name="login_email_label">What\'s your email</string>
|
<string name="login_email_label">What\'s your email</string>
|
||||||
<string name="confirm_password_label">Confirm password</string>
|
|
||||||
<string name="text_error_email_required">Email is required</string>
|
<string name="text_error_email_required">Email is required</string>
|
||||||
<string name="text_error_password_required">Password is required</string>
|
<string name="text_error_password_required">Password is required</string>
|
||||||
<string name="text_hint_email">Enter your email</string>
|
<string name="text_hint_email">Enter your email</string>
|
||||||
@@ -93,7 +92,7 @@
|
|||||||
<string name="reset_mail_send_failed">Failed to send email. Please check your network connection or try again later.</string>
|
<string name="reset_mail_send_failed">Failed to send email. Please check your network connection or try again later.</string>
|
||||||
<string name="seconds_ago">%1d seconds ago</string>
|
<string name="seconds_ago">%1d seconds ago</string>
|
||||||
<string name="minutes_ago">%1d minutes ago</string>
|
<string name="minutes_ago">%1d minutes ago</string>
|
||||||
<string name="private_policy_template">I agree to Paipai\'s</string>
|
<string name="private_policy_template">I agree to the</string>
|
||||||
<string name="private_policy_keyword">Rave Now’s Privacy Policy</string>
|
<string name="private_policy_keyword">Rave Now’s Privacy Policy</string>
|
||||||
<string name="gallery">Gallery</string>
|
<string name="gallery">Gallery</string>
|
||||||
<string name="chat_upper">CHAT</string>
|
<string name="chat_upper">CHAT</string>
|
||||||
@@ -116,10 +115,9 @@
|
|||||||
<string name="report_title">Reason for reporting this post?</string>
|
<string name="report_title">Reason for reporting this post?</string>
|
||||||
<string name="close">Close</string>
|
<string name="close">Close</string>
|
||||||
<string name="blocked">Blocked</string>
|
<string name="blocked">Blocked</string>
|
||||||
<string name="blocked_users">Blocked Users</string>
|
|
||||||
<string name="feedback">Feedback</string>
|
<string name="feedback">Feedback</string>
|
||||||
<string name="about_rave_now">About Rave Now </string>
|
<string name="about_rave_now">About Rave Now </string>
|
||||||
<string name="account_and_security">Account Security</string>
|
<string name="account_and_security">Account and security</string>
|
||||||
<string name="remove_account">Remove Account</string>
|
<string name="remove_account">Remove Account</string>
|
||||||
<string name="remove_account_desc">Are you sure you want to remove your account? This action cannot be undone.</string>
|
<string name="remove_account_desc">Are you sure you want to remove your account? This action cannot be undone.</string>
|
||||||
<string name="remove_account_password_hint">Please enter your password to confirm</string>
|
<string name="remove_account_password_hint">Please enter your password to confirm</string>
|
||||||
@@ -283,6 +281,16 @@
|
|||||||
<string name="group_chat_info_unlock_cost">Unlock cost: %1$d coins</string>
|
<string name="group_chat_info_unlock_cost">Unlock cost: %1$d coins</string>
|
||||||
<string name="group_chat_info_done">Done</string>
|
<string name="group_chat_info_done">Done</string>
|
||||||
<string name="group_chat_info_recharge_hint">You can recharge to get more coins</string>
|
<string name="group_chat_info_recharge_hint">You can recharge to get more coins</string>
|
||||||
|
|
||||||
|
<!-- Group Members -->
|
||||||
|
<string name="group_members_title">Group Members</string>
|
||||||
|
<string name="group_members_require_approval">Require approval to join</string>
|
||||||
|
<string name="group_members_you">You</string>
|
||||||
|
<string name="group_members_admin">Administrator</string>
|
||||||
|
<string name="group_members_list">Group members (%d)</string>
|
||||||
|
<string name="group_members_send_message">Send message</string>
|
||||||
|
<string name="group_members_delete_member">Delete Member</string>
|
||||||
|
|
||||||
<!-- Edit Profile Extras -->
|
<!-- Edit Profile Extras -->
|
||||||
<string name="mbti_type">MBTI</string>
|
<string name="mbti_type">MBTI</string>
|
||||||
<string name="zodiac">Zodiac</string>
|
<string name="zodiac">Zodiac</string>
|
||||||
@@ -297,5 +305,6 @@
|
|||||||
<string name="follow_system">Follow System</string>
|
<string name="follow_system">Follow System</string>
|
||||||
<string name="message_notification">Message Notification</string>
|
<string name="message_notification">Message Notification</string>
|
||||||
<string name="logout_confirm">Logout</string>
|
<string name="logout_confirm">Logout</string>
|
||||||
|
<string name="blocked_users">Blocked Users</string>
|
||||||
|
<string name="confirm_password_label">Confirm password</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user