Merge pull request #61 from Kevinlinpr/zhong_1

Zhong 1
This commit is contained in:
2025-11-10 21:28:21 +08:00
committed by GitHub
14 changed files with 2012 additions and 67 deletions

View File

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

View File

@@ -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>(

View File

@@ -1047,6 +1047,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")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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调用
}
}

View File

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

View File

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

View File

@@ -25,11 +25,10 @@
<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>
@@ -99,7 +98,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">Rave Nowのプライバシーポリシー</string> <string name="private_policy_keyword">Rave Nowのプライバシーポリシー</string>
<string name="gallery">ギャラリー</string> <string name="gallery">ギャラリー</string>
<string name="chat_upper">チャット</string> <string name="chat_upper">チャット</string>
@@ -122,10 +121,9 @@
<string name="report_title">この投稿を報告する理由は?</string> <string name="report_title">この投稿を報告する理由は?</string>
<string name="close">閉じる</string> <string name="close">閉じる</string>
<string name="blocked">ブロック済み</string> <string name="blocked">ブロック済み</string>
<string name="blocked_users">ブロック済みユーザー</string>
<string name="feedback">フィードバック</string> <string name="feedback">フィードバック</string>
<string name="about_rave_now">Rave Nowについて</string> <string name="about_rave_now">Rave Nowについて</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>
@@ -266,39 +264,6 @@
<string name="select_apply_to_use_theme">「適用」を選択してこのテーマを使用</string> <string name="select_apply_to_use_theme">「適用」を選択してこのテーマを使用</string>
<string name="tap_cancel_to_preview_other_themes">「キャンセル」をタップして他のテーマをプレビュー</string> <string name="tap_cancel_to_preview_other_themes">「キャンセル」をタップして他のテーマをプレビュー</string>
<!-- Edit Profile Extras -->
<string name="mbti_type">MBTIタイプ</string>
<string name="zodiac">星座</string>
<string name="change_cover">カバーを変更</string>
<string name="personal_intro">自己紹介</string>
<string name="error_nickname_empty">ニックネームは空にできません</string>
<string name="error_nickname_too_short">ニックネームの長さは3文字以上である必要があります</string>
<string name="error_nickname_too_long">ニックネームの長さは20文字以下である必要があります</string>
<string name="error_bio_too_long">自己紹介の長さは100文字以下である必要があります</string>
<string name="error_load_profile_failed">ユーザープロフィールの読み込みに失敗しました。もう一度お試しください</string>
<string name="save">保存</string>
<string name="choose_mbti">MBTIを選択</string>
<string name="choose_zodiac">星座を選択</string>
<string name="zodiac_aries">牡羊座</string>
<string name="zodiac_taurus">牡牛座</string>
<string name="zodiac_gemini">双子座</string>
<string name="zodiac_cancer">蟹座</string>
<string name="zodiac_leo">獅子座</string>
<string name="zodiac_virgo">乙女座</string>
<string name="zodiac_libra">天秤座</string>
<string name="zodiac_scorpio">蠍座</string>
<string name="zodiac_sagittarius">射手座</string>
<string name="zodiac_capricorn">山羊座</string>
<string name="zodiac_aquarius">水瓶座</string>
<string name="zodiac_pisces">魚座</string>
<!-- Side Menu -->
<string name="scan_qr">さっと動かす</string>
<string name="edit_profile_info">データの編集</string>
<string name="about_paipai">パイパイについて</string>
<string name="follow_system">フォローアップシステム</string>
<string name="message_notification">メッセージ通知</string>
<string name="logout_confirm">ログアウト</string>
<!-- Group Chat Info --> <!-- Group Chat Info -->
<string name="group_chat_info_title">グループチャット情報</string> <string name="group_chat_info_title">グループチャット情報</string>
<string name="group_chat_info_add_member">メンバーを追加</string> <string name="group_chat_info_add_member">メンバーを追加</string>
@@ -332,5 +297,31 @@
<string name="group_chat_info_unlock_cost">アンロック費用: %1$dコイン</string> <string name="group_chat_info_unlock_cost">アンロック費用: %1$dコイン</string>
<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>
<!-- 群聊成员 -->
<string name="group_members_title">グループチャットメンバー</string>
<string name="group_members_require_approval">承認が必要です</string>
<string name="group_members_you">あなた</string>
<string name="group_members_admin">管理者</string>
<string name="group_members_list">グループメンバー(%d</string>
<string name="group_members_send_message">メッセージを送信</string>
<string name="group_members_delete_member">メンバーを削除</string>
<!-- Edit Profile Extras -->
<string name="mbti_type">MBTI</string>
<string name="zodiac">星座</string>
<string name="save">保存</string>
<string name="choose_mbti">MBTIを選択</string>
<string name="choose_zodiac">星座を選択</string>
<!-- Side Menu -->
<string name="scan_qr">QRコードをスキャン</string>
<string name="edit_profile_info">プロフィールを編集</string>
<string name="about_paipai">パイパイについて</string>
<string name="follow_system">システムに従う</string>
<string name="message_notification">メッセージ通知</string>
<string name="logout_confirm">ログアウト</string>
<string name="blocked_users">ブロックされたユーザー</string>
<string name="confirm_password_label">パスワードを確認</string>
</resources> </resources>

View File

@@ -15,20 +15,19 @@
<string name="posts">帖子</string> <string name="posts">帖子</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>
@@ -83,7 +82,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>
@@ -100,7 +99,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>
@@ -124,9 +123,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>
@@ -150,8 +148,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>
@@ -329,6 +327,15 @@
<string name="zodiac_aquarius">水瓶座</string> <string name="zodiac_aquarius">水瓶座</string>
<string name="zodiac_pisces">双鱼座</string> <string name="zodiac_pisces">双鱼座</string>
<!-- 群聊成员 -->
<string name="group_members_title">群聊成员</string>
<string name="group_members_require_approval">须批准才能加入</string>
<string name="group_members_you"></string>
<string name="group_members_admin">管理员</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>
<string name="edit_profile_info">编辑资料</string> <string name="edit_profile_info">编辑资料</string>
@@ -336,4 +343,13 @@
<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>
<!-- Edit Profile Extras -->
<string name="mbti_type">MBTI</string>
<string name="zodiac">星座</string>
<string name="save">保存</string>
<string name="choose_mbti">选择 MBTI</string>
<string name="choose_zodiac">选择 Zodiac</string>
</resources> </resources>

View File

@@ -24,11 +24,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>
@@ -98,7 +97,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 Nows Privacy Policy</string> <string name="private_policy_keyword">Rave Nows 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>
@@ -121,10 +120,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>
@@ -297,6 +295,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>
@@ -330,5 +338,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>