Merge remote-tracking branch 'origin/main' into nagisa

This commit is contained in:
2025-11-12 18:15:49 +08:00
43 changed files with 2249 additions and 80 deletions

Binary file not shown.

View File

@@ -32,6 +32,7 @@ import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel
import com.aiosman.ravenow.ui.like.LikeNoticeViewModel import com.aiosman.ravenow.ui.like.LikeNoticeViewModel
import com.aiosman.ravenow.utils.Utils import com.aiosman.ravenow.utils.Utils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import com.aiosman.ravenow.im.OpenIMManager import com.aiosman.ravenow.im.OpenIMManager
import io.openim.android.sdk.OpenIMClient import io.openim.android.sdk.OpenIMClient
@@ -51,7 +52,7 @@ object AppState {
suspend fun initWithAccount(scope: CoroutineScope, context: Context) { suspend fun initWithAccount(scope: CoroutineScope, context: Context) {
// 如果是游客模式,使用简化的初始化流程 // 如果是游客模式,使用简化的初始化流程
if (AppStore.isGuest) { if (AppStore.isGuest) {
initWithGuestAccount() initWithGuestAccount(scope)
return return
} }
@@ -90,18 +91,50 @@ object AppState {
} catch (e: Exception) { } catch (e: Exception) {
Log.e("AppState", "刷新积分失败: ${e.message}") Log.e("AppState", "刷新积分失败: ${e.message}")
} }
// 并行加载积分规则和房间规则配置(不阻塞主流程)
scope.launch {
try {
PointService.refreshPointsRules()
} catch (e: Exception) {
Log.e("AppState", "加载积分规则失败: ${e.message}")
}
}
scope.launch {
try {
PointService.refreshRoomMaxMembers()
} catch (e: Exception) {
Log.e("AppState", "加载房间规则失败: ${e.message}")
}
}
} }
/** /**
* 游客模式的简化初始化 * 游客模式的简化初始化
*/ */
private fun initWithGuestAccount() { private fun initWithGuestAccount(scope: CoroutineScope) {
// 游客模式下不初始化推送和TRTC // 游客模式下不初始化推送和TRTC
// 设置默认的用户信息 // 设置默认的用户信息
UserId = 0 UserId = 0
profile = null profile = null
enableChat = false enableChat = false
Log.d("AppState", "Guest mode initialized without push notifications and TRTC") Log.d("AppState", "Guest mode initialized without push notifications and TRTC")
// 游客模式下也加载规则配置(用于查看费用信息)
scope.launch {
try {
PointService.refreshPointsRules()
} catch (e: Exception) {
Log.e("AppState", "加载积分规则失败: ${e.message}")
}
}
scope.launch {
try {
PointService.refreshRoomMaxMembers()
} catch (e: Exception) {
Log.e("AppState", "加载房间规则失败: ${e.message}")
}
}
} }
private suspend fun initChat(context: Context){ private suspend fun initChat(context: Context){

View File

@@ -69,6 +69,10 @@ data class AccountProfile(
val aiRoleAvatar: String? = null, val aiRoleAvatar: String? = null,
val aiRoleAvatarMedium: String? = null, val aiRoleAvatarMedium: String? = null,
val aiRoleAvatarLarge: String? = null, val aiRoleAvatarLarge: String? = null,
// 创建者信息仅AI账号有
@SerializedName("creatorProfile")
val creatorProfile: com.aiosman.ravenow.data.CreatorProfile? = null,
) { ) {
/** /**
* 转换为Entity * 转换为Entity
@@ -103,7 +107,8 @@ data class AccountProfile(
}, },
aiRoleAvatarLarge = aiRoleAvatarLarge?.let { aiRoleAvatarLarge = aiRoleAvatarLarge?.let {
if (it.isNotEmpty()) "${ApiClient.BASE_SERVER}$it" else null if (it.isNotEmpty()) "${ApiClient.BASE_SERVER}$it" else null
} },
creatorProfile = creatorProfile?.toCreatorProfileEntity()
) )
} }
} }

View File

@@ -14,6 +14,16 @@ interface DictService {
* 获取字典列表 * 获取字典列表
*/ */
suspend fun getDistList(keys: List<String>): List<DictItem> suspend fun getDistList(keys: List<String>): List<DictItem>
/**
* 获取外部字典项
*/
suspend fun getOutsideDictByKey(key: String): DictItem
/**
* 获取外部字典列表
*/
suspend fun getOutsideDistList(keys: List<String>): List<DictItem>
} }
class DictServiceImpl : DictService { class DictServiceImpl : DictService {
@@ -26,4 +36,13 @@ class DictServiceImpl : DictService {
val resp = ApiClient.api.getDicts(keys.joinToString(",")) val resp = ApiClient.api.getDicts(keys.joinToString(","))
return resp.body()?.list ?: throw Exception("failed to get dict list") return resp.body()?.list ?: throw Exception("failed to get dict list")
} }
override suspend fun getOutsideDictByKey(key: String): DictItem {
val resp = ApiClient.api.getOutsideDict(key)
return resp.body()?.data ?: throw Exception("failed to get outside dict")
}
override suspend fun getOutsideDistList(keys: List<String>): List<DictItem> {
val resp = ApiClient.api.getOutsideDicts(keys.joinToString(","))
return resp.body()?.list ?: throw Exception("failed to get outside dict list")
}
} }

View File

@@ -26,6 +26,36 @@ object PointService {
private val dictService: DictService = DictServiceImpl() private val dictService: DictService = DictServiceImpl()
private val gson = Gson() private val gson = Gson()
/**
* 积分规则key常量
* 对应积分规则JSON中的key值
*/
object PointsRuleKey {
// 获得积分类型add
/** 每日登录奖励 */
const val DAILY_LOGIN = "daily_login"
/** 用户注册奖励 */
const val USER_REGISTER = "user_register"
// 消费积分类型sub
/** 添加Agent记忆 */
const val ADD_AGENT_MEMORY = "add_agent_memory"
/** 增加房间容量 */
const val ADD_ROOM_CAP = "add_room_cap"
/** 创建房间 */
const val CREATE_ROOM = "create_room"
/** 创建定时事件 */
const val CREATE_SCHEDULE_EVENT = "create_schedule_event"
/** 房间私密模式 */
const val ROOM_PRIVATE = "room_private"
/** Agent私密模式 */
const val SPEND_AGENT_PRIVATE = "spend_agent_private"
/** 自定义聊天背景 */
const val SPEND_CHAT_BACKGROUND = "spend_chat_background"
/** 房间记忆添加 */
const val SPEND_ROOM_MEMORY = "spend_room_memory"
}
sealed class RuleAmount { sealed class RuleAmount {
data class Fixed(val value: Int) : RuleAmount() data class Fixed(val value: Int) : RuleAmount()
data class Range(val min: Int, val max: Int) : RuleAmount() data class Range(val min: Int, val max: Int) : RuleAmount()
@@ -50,6 +80,64 @@ object PointService {
} }
} }
// ========== 群聊人数限制(字典 points-rule相关 ==========
/**
* 群聊人数限制配置
* @param defaultMaxTotal 初始最大人数(默认值)
* @param maxTotal 最大人数(上限)
*/
data class RoomMaxMembers(
val defaultMaxTotal: Int,
val maxTotal: Int
)
private val _roomMaxMembers = MutableStateFlow<RoomMaxMembers?>(null)
val roomMaxMembers: StateFlow<RoomMaxMembers?> = _roomMaxMembers.asStateFlow()
/**
* 刷新群聊人数限制配置(从外部字典表加载 points-rule
* 加载时机与 refreshPointsRules 一致
*/
suspend fun refreshRoomMaxMembers(key: String = "points-rule") {
withContext(Dispatchers.IO) {
try {
val dict = dictService.getOutsideDictByKey(key)
val config = parseRoomMaxMembers(dict)
_roomMaxMembers.value = config
} catch (_: Exception) {
_roomMaxMembers.value = null
}
}
}
/**
* 解析群聊人数限制配置
* 解析格式:{"room":{"default":{"max-total":5},"max":{"max-total":200}}}
*/
private fun parseRoomMaxMembers(dict: DictItem): RoomMaxMembers? {
val raw = dict.value
val jsonStr = when (raw) {
is String -> raw
else -> gson.toJson(raw)
}
return try {
val root = JsonParser.parseString(jsonStr).asJsonObject
val roomObj = root.getAsJsonObject("room")
val defaultObj = roomObj?.getAsJsonObject("default")
val maxObj = roomObj?.getAsJsonObject("max")
val defaultMaxTotal = defaultObj?.get("max-total")?.takeIf { it.isJsonPrimitive }?.asInt ?: 5
val maxTotal = maxObj?.get("max-total")?.takeIf { it.isJsonPrimitive }?.asInt ?: 200
RoomMaxMembers(
defaultMaxTotal = defaultMaxTotal,
maxTotal = maxTotal
)
} catch (_: Exception) {
null
}
}
private fun parsePointsRules(dict: DictItem): PointsRules? { private fun parsePointsRules(dict: DictItem): PointsRules? {
val raw = dict.value val raw = dict.value
val jsonStr = when (raw) { val jsonStr = when (raw) {

View File

@@ -331,3 +331,21 @@ class RecommendationServiceImpl : RecommendationService {
} }
} }
/**
* CreatorProfile 扩展函数,转换为 CreatorProfileEntity
*/
fun CreatorProfile.toCreatorProfileEntity(): com.aiosman.ravenow.entity.CreatorProfileEntity {
return com.aiosman.ravenow.entity.CreatorProfileEntity(
id = id,
username = username,
nickname = nickname,
avatar = avatar?.let {
if (it.isNotEmpty()) "${ApiClient.BASE_SERVER}$it" else null
},
bio = bio,
trtcUserId = trtcUserId,
chatAIId = chatAIId,
aiAccount = aiAccount
)
}

View File

@@ -1335,6 +1335,16 @@ interface RaveNowAPI {
@Query("keys") keys: String @Query("keys") keys: String
): Response<ListContainer<DictItem>> ): Response<ListContainer<DictItem>>
@GET("/outside/dict")
suspend fun getOutsideDict(
@Query("key") key: String
): Response<DataContainer<DictItem>>
@GET("/outside/dicts")
suspend fun getOutsideDicts(
@Query("keys") keys: String
): Response<ListContainer<DictItem>>
@POST("captcha/generate") @POST("captcha/generate")
suspend fun generateCaptcha( suspend fun generateCaptcha(
@Body body: CaptchaRequestBody @Body body: CaptchaRequestBody
@@ -1500,6 +1510,35 @@ interface RaveNowAPI {
@Query("pageSize") pageSize: Int? = null @Query("pageSize") pageSize: Int? = null
): Response<ListContainer<Agent>> ): Response<ListContainer<Agent>>
/**
* 获取Prompt详情支持ID或OpenId
* @param promptId Prompt ID或OpenIdUUID格式
*/
@GET("outside/prompt/{promptId}")
suspend fun getPromptDetail(
@Path("promptId") promptId: String
): Response<DataContainer<Agent>>
/**
* 更新Prompt支持ID或OpenId
* @param promptId Prompt ID或OpenIdUUID格式
* @param avatar 头像文件(可选)
* @param title 标题(可选)
* @param desc 描述(可选)
* @param value 内容(可选)
* @param isPublic 是否公开(可选)
*/
@Multipart
@PATCH("outside/prompt/{promptId}")
suspend fun updatePrompt(
@Path("promptId") promptId: String,
@Part avatar: MultipartBody.Part?,
@Part("title") title: RequestBody?,
@Part("desc") desc: RequestBody?,
@Part("value") value: RequestBody?,
@Part("public") isPublic: RequestBody?,
): Response<DataContainer<Agent>>
// ========== Agent Rule API ========== // ========== Agent Rule API ==========
/** /**

View File

@@ -72,6 +72,9 @@ data class AccountProfileEntity(
val aiRoleAvatar: String? = null, val aiRoleAvatar: String? = null,
val aiRoleAvatarMedium: String? = null, val aiRoleAvatarMedium: String? = null,
val aiRoleAvatarLarge: String? = null, val aiRoleAvatarLarge: String? = null,
// 创建者信息仅AI账号有
val creatorProfile: CreatorProfileEntity? = null,
) )
/** /**
@@ -115,6 +118,28 @@ data class NoticeUserEntity(
val avatar: String, val avatar: String,
) )
/**
* 创建者信息
*/
data class CreatorProfileEntity(
// 用户ID
val id: Long,
// 用户名
val username: String? = null,
// 昵称
val nickname: String,
// 头像
val avatar: String? = null,
// 个人简介
val bio: String? = null,
// trtcUserId
val trtcUserId: String? = null,
// chatAIId
val chatAIId: String? = null,
// 是否为AI账号
val aiAccount: Boolean = false,
)
/** /**
* 用户点赞消息分页数据加载器 * 用户点赞消息分页数据加载器
*/ */

View File

@@ -1,8 +1,12 @@
package com.aiosman.ravenow.entity package com.aiosman.ravenow.entity
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.aiosman.ravenow.data.ListContainer import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.Room
import com.aiosman.ravenow.data.ServiceException import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import java.io.IOException
/** /**
* 群聊房间 * 群聊房间
@@ -253,3 +257,58 @@ class RoomLoader : DataLoader<AgentEntity,AgentLoaderExtraArgs>() {
} }
/**
* 房间远程数据源
*/
class RoomRemoteDataSource {
suspend fun searchRooms(
pageNumber: Int,
pageSize: Int = 20,
search: String
): ListContainer<RoomEntity>? {
val resp = ApiClient.api.getRooms(
page = pageNumber,
pageSize = pageSize,
search = search,
roomType = "public" // 搜索时只显示公有房间
)
val body = resp.body() ?: return null
return ListContainer(
total = body.total,
page = pageNumber,
pageSize = pageSize,
list = body.list.map { it.toRoomtEntity() }
)
}
}
/**
* 房间搜索分页加载器
*/
class RoomSearchPagingSource(
private val roomRemoteDataSource: RoomRemoteDataSource,
private val keyword: String,
) : PagingSource<Int, RoomEntity>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, RoomEntity> {
return try {
val currentPage = params.key ?: 1
val rooms = roomRemoteDataSource.searchRooms(
pageNumber = currentPage,
pageSize = params.loadSize,
search = keyword
)
LoadResult.Page(
data = rooms?.list ?: listOf(),
prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = if (rooms?.list?.isNotEmpty() == true) currentPage + 1 else null
)
} catch (exception: IOException) {
LoadResult.Error(exception)
}
}
override fun getRefreshKey(state: PagingState<Int, RoomEntity>): Int? {
return state.anchorPosition
}
}

View File

@@ -41,6 +41,7 @@ import com.aiosman.ravenow.ui.account.ResetPasswordScreen
import com.aiosman.ravenow.ui.account.ZodiacSelectScreen import com.aiosman.ravenow.ui.account.ZodiacSelectScreen
import com.aiosman.ravenow.ui.agent.AddAgentScreen import com.aiosman.ravenow.ui.agent.AddAgentScreen
import com.aiosman.ravenow.ui.agent.AgentImageCropScreen import com.aiosman.ravenow.ui.agent.AgentImageCropScreen
import com.aiosman.ravenow.ui.agent.AiPromptEditScreen
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
import com.aiosman.ravenow.ui.chat.ChatAiScreen import com.aiosman.ravenow.ui.chat.ChatAiScreen
import com.aiosman.ravenow.ui.chat.ChatSettingScreen import com.aiosman.ravenow.ui.chat.ChatSettingScreen
@@ -133,6 +134,7 @@ sealed class NavigationRoute(
data object MbtiSelect : NavigationRoute("MbtiSelect") data object MbtiSelect : NavigationRoute("MbtiSelect")
data object ZodiacSelect : NavigationRoute("ZodiacSelect") data object ZodiacSelect : NavigationRoute("ZodiacSelect")
data object ScanQr : NavigationRoute("ScanQr") data object ScanQr : NavigationRoute("ScanQr")
data object AiPromptEdit : NavigationRoute("AiPromptEdit/{chatAIId}")
} }
@@ -684,6 +686,18 @@ fun NavigationController(
} }
} }
composable(
route = NavigationRoute.AiPromptEdit.route,
arguments = listOf(navArgument("chatAIId") { type = NavType.StringType })
) {
val chatAIId = it.arguments?.getString("chatAIId") ?: ""
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
AiPromptEditScreen(chatAIId = chatAIId)
}
}
} }
} }

View File

@@ -20,12 +20,14 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.ConstVars import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.data.api.ErrorCode import com.aiosman.ravenow.data.api.ErrorCode
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
@@ -99,10 +101,11 @@ fun ResetPasswordScreen() {
if (e.code == ErrorCode.USER_NOT_EXIST.code){ if (e.code == ErrorCode.USER_NOT_EXIST.code){
usernameError = context.getString(R.string.error_40002_user_not_exist) usernameError = context.getString(R.string.error_40002_user_not_exist)
} else { } else {
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() // 其他错误不显示Toast
isSendSuccess = false
} }
} catch (e: Exception) { } catch (e: Exception) {
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() // 异常错误不显示Toast
isSendSuccess = false isSendSuccess = false
} finally { } finally {
isLoading = false isLoading = false
@@ -133,12 +136,21 @@ fun ResetPasswordScreen() {
modifier = Modifier.padding(horizontal = 24.dp), modifier = Modifier.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// 暗色模式下的 hint 文本颜色
val isDarkMode = AppState.darkMode
val hintColor = if (isDarkMode) {
Color(0xFFFFFFFF).copy(alpha = 0.7f)
} else {
null // 使用默认颜色
}
TextInputField( TextInputField(
text = username, text = username,
onValueChange = { username = it }, onValueChange = { username = it },
hint = stringResource(R.string.text_hint_email), hint = stringResource(R.string.text_hint_email),
enabled = !isLoading && countDown == null, enabled = !isLoading && countDown == null,
error = usernameError, error = usernameError,
customHintColor = hintColor
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Box( Box(
@@ -178,9 +190,11 @@ fun ResetPasswordScreen() {
} else { } else {
stringResource(R.string.recover) stringResource(R.string.recover)
}, },
backgroundColor = appColors.main, backgroundColor = Color(0xFF7C45ED), // 紫色背景
loadingBackgroundColor = Color(0xFF7C45ED), // loading 时保持紫色
disabledBackgroundColor = Color(0xFF7C45ED), // disabled 时保持紫色
color = appColors.mainText, color = appColors.mainText,
isLoading = isLoading, isLoading = isLoading && countDown == null, // 只在未发送成功时显示loading
contentPadding = PaddingValues(0.dp), contentPadding = PaddingValues(0.dp),
enabled = countDown == null, enabled = countDown == null,
) { ) {
@@ -193,6 +207,8 @@ fun ResetPasswordScreen() {
.fillMaxWidth() .fillMaxWidth()
.height(48.dp), .height(48.dp),
text = stringResource(R.string.back_upper), text = stringResource(R.string.back_upper),
backgroundColor = Color(0xFF7C45ED), // 紫色背景
color = Color.White, // 白色文字
contentPadding = PaddingValues(0.dp), contentPadding = PaddingValues(0.dp),
) { ) {
navController.navigateUp() navController.navigateUp()

View File

@@ -59,6 +59,7 @@ import java.io.InputStream
/** /**
* 专门用于智能体头像裁剪的页面 * 专门用于智能体头像裁剪的页面
* 支持创建和编辑两种模式
*/ */
@Composable @Composable
fun AgentImageCropScreen() { fun AgentImageCropScreen() {
@@ -71,6 +72,14 @@ fun AgentImageCropScreen() {
val density = LocalDensity.current val density = LocalDensity.current
val navController = LocalNavController.current val navController = LocalNavController.current
// 检查是否在编辑模式通过检查是否有编辑ViewModel的实例
val isEditMode = remember {
// 通过检查导航栈或使用其他方式判断
// 暂时使用一个简单的方法检查AddAgentViewModel是否正在选择头像
// 如果不是,则可能是编辑模式
!AddAgentViewModel.isSelectingAvatar
}
val imagePickLauncher = rememberLauncherForActivityResult( val imagePickLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent() contract = ActivityResultContracts.GetContent()
) { uri: Uri? -> ) { uri: Uri? ->
@@ -84,7 +93,9 @@ fun AgentImageCropScreen() {
} }
if (uri == null) { if (uri == null) {
// 用户取消选择图片,重置标志 // 用户取消选择图片,重置标志
AddAgentViewModel.isSelectingAvatar = false if (!isEditMode) {
AddAgentViewModel.isSelectingAvatar = false
}
navController.popBackStack() navController.popBackStack()
} }
} }
@@ -122,7 +133,9 @@ fun AgentImageCropScreen() {
contentDescription = null, contentDescription = null,
modifier = Modifier.clickable { modifier = Modifier.clickable {
// 用户取消头像选择,重置标志 // 用户取消头像选择,重置标志
AddAgentViewModel.isSelectingAvatar = false if (!isEditMode) {
AddAgentViewModel.isSelectingAvatar = false
}
navController.popBackStack() navController.popBackStack()
}, },
colorFilter = ColorFilter.tint(Color.White) colorFilter = ColorFilter.tint(Color.White)
@@ -137,13 +150,21 @@ fun AgentImageCropScreen() {
modifier = Modifier.clickable { modifier = Modifier.clickable {
if (croppedBitmap != null) { if (croppedBitmap != null) {
// 如果已经有裁剪结果,直接返回 // 如果已经有裁剪结果,直接返回
AddAgentViewModel.croppedBitmap = croppedBitmap if (isEditMode) {
// 重置头像选择标志 // 编辑模式需要找到当前的编辑ViewModel实例
AddAgentViewModel.isSelectingAvatar = false // 由于无法直接访问,我们使用一个全局状态或者通过其他方式传递
AddAgentViewModel.viewModelScope.launch { // 暂时先保存到AddAgentViewModel,编辑页面会检查
AddAgentViewModel.updateAgentAvatar(context) AddAgentViewModel.croppedBitmap = croppedBitmap
navController.popBackStack() } else {
AddAgentViewModel.croppedBitmap = croppedBitmap
// 重置头像选择标志
AddAgentViewModel.isSelectingAvatar = false
AddAgentViewModel.viewModelScope.launch {
AddAgentViewModel.updateAgentAvatar(context)
navController.popBackStack()
}
} }
navController.popBackStack()
} else { } else {
// 进行裁剪 // 进行裁剪
imageCrop?.let { imageCrop?.let {

View File

@@ -0,0 +1,483 @@
package com.aiosman.ravenow.ui.agent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
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.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import com.aiosman.ravenow.LocalNavController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.ActionButton
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.PointsPaymentDialog
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.composables.form.FormTextInput
import com.aiosman.ravenow.ui.composables.form.FormTextInput2
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.data.PointService
import androidx.compose.runtime.collectAsState
import kotlinx.coroutines.launch
/**
* AI Prompt 编辑页面
*/
@Composable
fun AiPromptEditScreen(
chatAIId: String,
viewModel: AiPromptEditViewModel = viewModel()
) {
val context = LocalContext.current
val navController = LocalNavController.current
val appColors = LocalAppTheme.current
// 加载Prompt详情
LaunchedEffect(chatAIId) {
viewModel.loadPromptDetail(chatAIId)
}
// 监听头像裁剪结果从AgentImageCropScreen返回
LaunchedEffect(viewModel.isSelectingAvatar) {
if (!viewModel.isSelectingAvatar && AddAgentViewModel.croppedBitmap != null) {
// 从裁剪页面返回,检查是否有新的裁剪结果
viewModel.croppedBitmap = AddAgentViewModel.croppedBitmap
// 清空AddAgentViewModel的裁剪结果避免影响创建页面
AddAgentViewModel.croppedBitmap = null
}
}
// 状态
var showPrivacyConfirmDialog by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
val scope = rememberCoroutineScope()
// 获取积分规则和余额
val pointsRules by PointService.pointsRules.collectAsState(initial = null)
val pointsBalance by PointService.pointsBalance.collectAsState(initial = null)
// 计算是否需要付费
val needsPayment = viewModel.needsPrivacyPayment()
val privacyCost = viewModel.getPrivacyCost()
val currentBalance = viewModel.getCurrentBalance()
val balanceAfterCost = viewModel.calculateBalanceAfterCost(privacyCost)
val isBalanceSufficient = viewModel.isBalanceSufficient(privacyCost)
Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color(0xFFFAFAFB)),
horizontalAlignment = Alignment.CenterHorizontally
) {
StatusBarSpacer()
// 顶部导航栏
Box(
modifier = Modifier
.fillMaxWidth()
.background(color = Color(0xFFFAFAFB))
.padding(horizontal = 14.dp, vertical = 16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = "返回",
modifier = Modifier
.size(24.dp)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
navController.navigateUp()
},
colorFilter = ColorFilter.tint(appColors.text)
)
Spacer(modifier = Modifier.size(12.dp))
Text(
"编辑Ai",
fontWeight = FontWeight.W600,
modifier = Modifier.weight(1f),
fontSize = 17.sp,
color = appColors.text
)
}
}
Spacer(modifier = Modifier.height(1.dp))
// 内容区域
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.background(Color(0xFFFAFAFB))
) {
// 头像选择
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 18.dp),
horizontalAlignment = Alignment.Start
) {
Text(
text = stringResource(R.string.avatar),
fontSize = 12.sp,
color = appColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(4.dp))
Box(
modifier = Modifier
.size(72.dp)
.clip(CircleShape)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0x777c45ed),
Color(0x777c68ef),
Color(0x557bd8f8)
)
)
)
.noRippleClickable {
viewModel.isSelectingAvatar = true
// 标记为编辑模式
AddAgentViewModel.isSelectingAvatar = false
navController.navigate(NavigationRoute.AgentImageCrop.route)
},
contentAlignment = Alignment.Center
) {
when {
viewModel.croppedBitmap != null -> {
Image(
bitmap = viewModel.croppedBitmap!!.asImageBitmap(),
contentDescription = "Avatar",
modifier = Modifier
.size(72.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
}
viewModel.avatarUrl != null -> {
CustomAsyncImage(
context = context,
imageUrl = viewModel.avatarUrl!!,
contentDescription = "Avatar",
modifier = Modifier
.size(72.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
}
else -> {
Image(
painter = painterResource(id = R.mipmap.icons_infor_edit),
contentDescription = "Edit",
colorFilter = ColorFilter.tint(Color.White),
modifier = Modifier.size(20.dp),
)
}
}
}
}
Spacer(modifier = Modifier.height(18.dp))
// 名称输入
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
Text(
text = stringResource(R.string.agent_name),
fontSize = 12.sp,
color = appColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(4.dp))
FormTextInput(
value = viewModel.title,
hint = stringResource(R.string.agent_name_hint_1),
background = Color.White,
modifier = Modifier.fillMaxWidth(),
) { value ->
viewModel.title = value
}
}
Spacer(modifier = Modifier.height(18.dp))
// 描述输入
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
Text(
text = stringResource(R.string.agent_desc),
fontSize = 12.sp,
color = appColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(4.dp))
FormTextInput2(
value = viewModel.desc,
hint = stringResource(R.string.agent_desc_hint),
background = Color.White,
modifier = Modifier.fillMaxWidth(),
) { value ->
viewModel.desc = value
}
}
Spacer(modifier = Modifier.height(18.dp))
// 设定权限区域
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
Text(
text = "设定权限",
fontSize = 12.sp,
color = appColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
// 公开/私有切换
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(25.dp))
.background(Color.White)
.border(
width = 1.dp,
color = Color(red = 124f / 255f, green = 116f / 255f, blue = 128f / 255f, alpha = 0.08f),
shape = RoundedCornerShape(25.dp)
)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = if (viewModel.isPublic) "公开" else "私有",
fontSize = 14.sp,
color = appColors.text,
fontWeight = FontWeight.W500
)
Switch(
checked = viewModel.isPublic,
onCheckedChange = { checked ->
if (!checked && needsPayment && !viewModel.paidForPrivacyEdit) {
// 需要付费,显示确认对话框
showPrivacyConfirmDialog = true
} else {
viewModel.isPublic = checked
}
},
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = appColors.brandColorsColor,
uncheckedThumbColor = Color.White,
uncheckedTrackColor = appColors.brandColorsColor.copy(alpha = 0.5f),
uncheckedBorderColor = Color.Transparent
),
modifier = Modifier.size(width = 64.dp, height = 28.dp)
)
}
// 首次解锁AI权限提示
if (needsPayment && !viewModel.paidForPrivacyEdit && privacyCost > 0) {
Spacer(modifier = Modifier.height(8.dp))
// 主要内容容器(去掉阴影)
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(
color = Color(red = 251f / 255f, green = 248f / 255f, blue = 239f / 255f)
)
.border(
width = 1.dp,
color = Color(red = 243f / 255f, green = 234f / 255f, blue = 206f / 255f),
shape = RoundedCornerShape(16.dp)
)
.padding(12.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// 锁图标容器
Box(
modifier = Modifier
.size(32.dp)
.background(
color = Color(red = 1f, green = 204f / 255f, blue = 0f, alpha = 0.12f),
shape = RoundedCornerShape(10.7.dp)
),
contentAlignment = Alignment.Center
) {
// 锁图标(使用文本代替,实际项目中可以使用图片资源)
Text(
text = "🔒",
fontSize = 18.sp
)
}
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "首次解锁Ai权限",
fontSize = 13.sp,
color = Color(red = 172f / 255f, green = 127f / 255f, blue = 94f / 255f),
fontWeight = FontWeight.W500
)
Spacer(modifier = Modifier.height(4.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "将消耗",
fontSize = 12.sp,
color = Color(red = 172f / 255f, green = 127f / 255f, blue = 94f / 255f)
)
Text(
text = "$privacyCost",
fontSize = 12.sp,
color = Color(red = 1f, green = 141f / 255f, blue = 40f / 255f)
)
// 小硬币图标
Box(
modifier = Modifier
.size(16.dp)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFFFFD700),
Color(0xFFFFA500)
)
),
shape = CircleShape
)
)
Text(
text = "解锁后可随时切换",
fontSize = 12.sp,
color = Color(red = 172f / 255f, green = 127f / 255f, blue = 94f / 255f)
)
}
}
}
}
}
}
}
// 底部保存按钮
Box(
modifier = Modifier
.fillMaxWidth()
.background(color = Color(0xFFFAFAFB))
.padding(horizontal = 16.dp, vertical = 16.dp)
) {
ActionButton(
text = "保存",
enabled = !viewModel.isUpdating && !viewModel.isLoading,
isLoading = viewModel.isUpdating,
modifier = Modifier.fillMaxWidth()
) {
// 验证输入
val validationError = viewModel.validate()
if (validationError != null) {
errorMessage = validationError
return@ActionButton
}
// 检查是否需要付费确认
if (needsPayment && !viewModel.paidForPrivacyEdit) {
showPrivacyConfirmDialog = true
return@ActionButton
}
// 执行保存
scope.launch {
try {
viewModel.updatePrompt(context)
navController.navigateUp()
} catch (e: Exception) {
errorMessage = e.message ?: "保存失败"
}
}
}
}
}
// 隐私权限付费确认对话框
if (showPrivacyConfirmDialog) {
PointsPaymentDialog(
cost = privacyCost,
currentBalance = currentBalance,
balanceAfterCost = balanceAfterCost,
isBalanceSufficient = isBalanceSufficient,
onConfirm = {
showPrivacyConfirmDialog = false
scope.launch {
try {
viewModel.isPublic = false
viewModel.updatePrompt(context)
viewModel.paidForPrivacyEdit = true
navController.navigateUp()
} catch (e: Exception) {
errorMessage = e.message ?: "保存失败"
}
}
},
onCancel = {
showPrivacyConfirmDialog = false
},
title = "首次解锁AI权限",
description = "将消耗 $privacyCost 派币解锁后可随时切换"
)
}
// 错误提示
errorMessage?.let { error ->
LaunchedEffect(error) {
kotlinx.coroutines.delay(3000)
errorMessage = null
}
// TODO: 显示Toast或Snackbar
}
}

View File

@@ -0,0 +1,208 @@
package com.aiosman.ravenow.ui.agent
import android.content.Context
import android.graphics.Bitmap
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.data.UploadImage
import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.PointService
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.entity.AgentEntity
import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
class AiPromptEditViewModel : ViewModel() {
var chatAIId by mutableStateOf("")
var title by mutableStateOf("")
var desc by mutableStateOf("")
var isPublic by mutableStateOf(true)
var originalIsPublic by mutableStateOf(true)
var paidForPrivacyEdit by mutableStateOf(false)
var avatarUrl by mutableStateOf<String?>(null)
var croppedBitmap by mutableStateOf<Bitmap?>(null)
var isUpdating by mutableStateOf(false)
var isLoading by mutableStateOf(false)
var errorMessage by mutableStateOf<String?>(null)
var isSelectingAvatar by mutableStateOf(false)
/**
* 加载Prompt详情
*/
fun loadPromptDetail(chatAIId: String) {
viewModelScope.launch {
try {
isLoading = true
errorMessage = null
this@AiPromptEditViewModel.chatAIId = chatAIId
val response = ApiClient.api.getPromptDetail(chatAIId)
val body = response.body()?.data ?: throw ServiceException("Failed to get prompt detail")
// 填充数据
title = body.title
desc = body.desc
isPublic = body.isPublic
originalIsPublic = body.isPublic
avatarUrl = "${ApiClient.BASE_API_URL}/outside${body.avatar}?token=${com.aiosman.ravenow.AppStore.token}"
// 注意Agent数据模型可能没有paidForPrivacyEdit字段需要从其他地方获取
// 暂时设为false后续可以根据实际API响应调整
paidForPrivacyEdit = false
} catch (e: Exception) {
Log.e("AiPromptEditViewModel", "Error loading prompt detail", e)
errorMessage = "加载失败: ${e.message}"
} finally {
isLoading = false
}
}
}
/**
* 更新Prompt
*/
suspend fun updatePrompt(context: Context): AgentEntity? {
try {
isUpdating = true
errorMessage = null
// 准备头像文件
val avatarFile = if (croppedBitmap != null) {
val file = File(context.cacheDir, "agent_avatar_edit.jpg")
croppedBitmap!!.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
UploadImage(file, "agent_avatar_edit.jpg", "", "jpg")
} else {
null
}
// 准备请求参数
val textTitle = title.trim().toRequestBody("text/plain".toMediaTypeOrNull())
val textDesc = desc.trim().toRequestBody("text/plain".toMediaTypeOrNull())
val textValue = desc.trim().toRequestBody("text/plain".toMediaTypeOrNull()) // value通常和desc相同
val isPublicBody = isPublic.toString().toRequestBody("text/plain".toMediaTypeOrNull())
val avatarPart: MultipartBody.Part? = avatarFile?.let {
val requestFile = it.file.asRequestBody("image/*".toMediaTypeOrNull())
MultipartBody.Part.createFormData("avatar", it.filename, requestFile)
}
// 调用更新API
val response = ApiClient.api.updatePrompt(
promptId = chatAIId,
avatar = avatarPart,
title = textTitle,
desc = textDesc,
value = textValue,
isPublic = isPublicBody
)
val body = response.body()?.data ?: throw ServiceException("Failed to update prompt")
// 更新本地状态
originalIsPublic = isPublic
return body.toAgentEntity()
} catch (e: Exception) {
Log.e("AiPromptEditViewModel", "Error updating prompt", e)
errorMessage = "更新失败: ${e.message}"
throw e
} finally {
isUpdating = false
}
}
/**
* 验证输入
*/
fun validate(): String? {
return when {
title.trim().isEmpty() -> "智能体名称不能为空"
title.trim().length < 2 -> "智能体名称长度不能少于2个字符"
title.trim().length > 20 -> "智能体名称长度不能超过20个字符"
desc.trim().isEmpty() -> "智能体描述不能为空"
desc.trim().length > 512 -> "智能体描述长度不能超过512个字符"
else -> null
}
}
/**
* 判断是否需要付费解锁隐私切换
*/
fun needsPrivacyPayment(): Boolean {
// 如果已经解锁过,则不需要付费
if (paidForPrivacyEdit) {
return false
}
// 只有从公开true切换到私有false才需要付费
return originalIsPublic == true && isPublic == false
}
/**
* 获取解锁隐私权限的费用
* @return 费用金额,如果无法获取则返回 0
*/
fun getPrivacyCost(): Int {
val rules = PointService.pointsRules.value
val costRule = rules?.sub?.get(PointService.PointsRuleKey.SPEND_AGENT_PRIVATE)
return when (costRule) {
is PointService.RuleAmount.Fixed -> costRule.value
is PointService.RuleAmount.Range -> costRule.min // 使用最小值作为默认费用
null -> 0
}
}
/**
* 获取当前余额
* @return 当前余额,如果无法获取则返回 0
*/
fun getCurrentBalance(): Int {
return PointService.pointsBalance.value?.balance ?: 0
}
/**
* 计算消耗后余额
* @param cost 费用
* @return 消耗后余额
*/
fun calculateBalanceAfterCost(cost: Int): Int {
val currentBalance = getCurrentBalance()
return (currentBalance - cost).coerceAtLeast(0)
}
/**
* 检查余额是否充足
* @param cost 费用
* @return 是否充足
*/
fun isBalanceSufficient(cost: Int): Boolean {
return getCurrentBalance() >= cost
}
/**
* 清空数据
*/
fun clearData() {
chatAIId = ""
title = ""
desc = ""
isPublic = true
originalIsPublic = true
paidForPrivacyEdit = false
avatarUrl = null
croppedBitmap = null
isUpdating = false
isLoading = false
errorMessage = null
isSelectingAvatar = false
}
}

View File

@@ -0,0 +1,342 @@
package com.aiosman.ravenow.ui.composables
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
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.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
/**
* 全局付费确认对话框组件
* 参考 iOS 版本的 PointsConfirmDialog
*
* @param cost 需要支付的费用
* @param currentBalance 当前余额
* @param balanceAfterCost 支付后余额
* @param isBalanceSufficient 余额是否充足
* @param onConfirm 确认支付回调
* @param onCancel 取消回调
* @param title 对话框标题
* @param description 对话框描述
* @param isProcessing 是否正在处理中
*/
@Composable
fun PointsPaymentDialog(
cost: Int,
currentBalance: Int,
balanceAfterCost: Int,
isBalanceSufficient: Boolean,
onConfirm: () -> Unit,
onCancel: () -> Unit,
title: String,
description: String,
isProcessing: Boolean = false
) {
val appColors = LocalAppTheme.current
val configuration = LocalConfiguration.current
val screenWidth = configuration.screenWidthDp.dp
val dialogWidth = (screenWidth - 48.dp).coerceAtMost(360.dp)
Dialog(
onDismissRequest = {
if (!isProcessing) {
onCancel()
}
},
properties = DialogProperties(
dismissOnBackPress = !isProcessing,
dismissOnClickOutside = !isProcessing
)
) {
Card(
modifier = Modifier
.width(dialogWidth)
.shadow(
elevation = 20.dp,
shape = RoundedCornerShape(20.dp),
spotColor = Color.Black.copy(alpha = 0.2f)
),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(
containerColor = appColors.background
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 顶部图标 - 使用 paip_coin_img
Spacer(modifier = Modifier.height(32.dp))
Image(
painter = painterResource(id = R.mipmap.paip_coin_img),
contentDescription = null,
modifier = Modifier.size(80.dp),
contentScale = ContentScale.Fit
)
// 标题
Spacer(modifier = Modifier.height(16.dp))
Text(
text = title,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = appColors.text,
textAlign = TextAlign.Center
)
// 描述
Spacer(modifier = Modifier.height(8.dp))
Text(
text = description,
fontSize = 14.sp,
color = appColors.secondaryText,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 20.dp)
)
// 积分消耗信息区域
Spacer(modifier = Modifier.height(24.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = appColors.inputBackground.copy(alpha = 0.5f),
shape = RoundedCornerShape(12.dp)
)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// 需要消耗
CostInfoRow(
label = stringResource(R.string.cost_required),
amount = cost,
appColors = appColors,
amountColor = Color(0xFFFF8C00) // 橙色
)
HorizontalDivider(color = appColors.divider)
// 当前余额
CostInfoRow(
label = stringResource(R.string.current_balance),
amount = currentBalance,
appColors = appColors,
amountColor = if (isBalanceSufficient) appColors.text else Color.Red
)
HorizontalDivider(color = appColors.divider)
// 支付后余额
CostInfoRow(
label = stringResource(R.string.balance_after),
amount = balanceAfterCost,
appColors = appColors,
amountColor = appColors.text
)
}
// 余额不足提示
if (!isBalanceSufficient) {
Spacer(modifier = Modifier.height(12.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
tint = Color(0xFFFF8C00), // 橙色
modifier = Modifier.size(16.dp)
)
Text(
text = stringResource(R.string.insufficient_pai_coin_balance),
fontSize = 13.sp,
color = Color(0xFFFF8C00), // 橙色
textAlign = TextAlign.Center
)
}
}
// 按钮
Spacer(modifier = Modifier.height(24.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// 取消按钮
Button(
onClick = {
if (!isProcessing) {
onCancel()
}
},
modifier = Modifier
.weight(1f)
.height(50.dp),
enabled = !isProcessing,
colors = ButtonDefaults.buttonColors(
containerColor = appColors.inputBackground,
contentColor = appColors.text,
disabledContainerColor = appColors.inputBackground,
disabledContentColor = appColors.text.copy(alpha = 0.5f)
),
shape = RoundedCornerShape(12.dp)
) {
Text(
text = stringResource(R.string.cancel),
fontSize = 16.sp,
fontWeight = FontWeight.W500
)
}
// 确认按钮
Box(
modifier = Modifier
.weight(1f)
.height(50.dp)
.background(
brush = if (isBalanceSufficient) {
Brush.horizontalGradient(
colors = listOf(
appColors.main,
appColors.main
)
)
} else {
Brush.horizontalGradient(
colors = listOf(
Color(0xFFFF8C00), // 橙色
Color.Red
)
)
},
shape = RoundedCornerShape(12.dp)
)
.then(
if (!isProcessing) {
Modifier.noRippleClickable {
if (!isBalanceSufficient) {
// 积分不足,跳转充值页面
onCancel()
// 这里可以发送通知或回调来跳转充值页面
} else {
// 积分充足,确认消费
onConfirm()
}
}
} else {
Modifier
}
),
contentAlignment = Alignment.Center
) {
if (isProcessing) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = Color.White,
strokeWidth = 2.dp
)
} else {
Text(
text = if (isBalanceSufficient) {
stringResource(R.string.confirm_consumption)
} else {
stringResource(R.string.go_recharge)
},
fontSize = 16.sp,
fontWeight = FontWeight.W600,
color = Color.White,
textAlign = TextAlign.Center
)
}
}
}
Spacer(modifier = Modifier.height(32.dp))
}
}
}
}
/**
* 费用信息行组件
*/
@Composable
private fun CostInfoRow(
label: String,
amount: Int,
appColors: com.aiosman.ravenow.AppThemeData,
amountColor: Color? = null
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = label,
fontSize = 14.sp,
color = appColors.secondaryText
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
// 星形图标(参考 iOS 版本)
Icon(
imageVector = Icons.Default.Star,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = Color(0xFFFFD700) // 黄色
)
Text(
text = "${amount.formatNumber()}",
fontSize = 16.sp,
fontWeight = FontWeight.W600,
color = amountColor ?: appColors.text
)
Text(
text = stringResource(R.string.pai_coin),
fontSize = 14.sp,
color = appColors.secondaryText
)
}
}
}
/**
* 格式化数字,添加千位分隔符
*/
private fun Int.formatNumber(): String {
return this.toString().reversed().chunked(3).joinToString(",").reversed()
}

View File

@@ -97,7 +97,7 @@ fun PolicyCheckbox(
addStyle( addStyle(
style = SpanStyle( style = SpanStyle(
color = appColor.main, color = Color(0xFF7C45ED), // 紫色
textDecoration = TextDecoration.Underline textDecoration = TextDecoration.Underline
), ),
start = template.length + 1, start = template.length + 1,

View File

@@ -67,6 +67,13 @@ fun FormTextInput(
.let { .let {
if (error != null) { if (error != null) {
it.border(1.dp, AppColors.error, RoundedCornerShape(24.dp)) it.border(1.dp, AppColors.error, RoundedCornerShape(24.dp))
} else if (background != null && background == Color.White) {
// 如果传入白色背景,添加灰色边框
it.border(
1.dp,
Color(red = 124f / 255f, green = 116f / 255f, blue = 128f / 255f, alpha = 0.08f),
RoundedCornerShape(25.dp)
)
} else { } else {
it it
} }

View File

@@ -68,6 +68,13 @@ fun FormTextInput2(
.let { .let {
if (error != null) { if (error != null) {
it.border(1.dp, AppColors.error, RoundedCornerShape(24.dp)) it.border(1.dp, AppColors.error, RoundedCornerShape(24.dp))
} else if (background != null && background == Color.White) {
// 如果传入白色背景,添加灰色边框
it.border(
1.dp,
Color(red = 124f / 255f, green = 116f / 255f, blue = 128f / 255f, alpha = 0.08f),
RoundedCornerShape(25.dp)
)
} else { } else {
it it
} }

View File

@@ -3,6 +3,7 @@ package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -14,11 +15,15 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
@@ -27,11 +32,15 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.data.PointService
import com.aiosman.ravenow.ui.composables.CustomAsyncImage 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.composables.TabItem import com.aiosman.ravenow.ui.composables.TabItem
@@ -91,6 +100,19 @@ fun CreateGroupChatScreen() {
val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
// 获取费用和余额信息
val pointsRules by PointService.pointsRules.collectAsState(initial = null)
val pointsBalance by PointService.pointsBalance.collectAsState(initial = null)
val roomMaxMembers by PointService.roomMaxMembers.collectAsState(initial = null)
val cost = CreateGroupChatViewModel.getCreateRoomCost()
val currentBalance = CreateGroupChatViewModel.getCurrentBalance()
val balanceAfterCost = CreateGroupChatViewModel.calculateBalanceAfterCost(cost)
val isBalanceSufficient = CreateGroupChatViewModel.isBalanceSufficient(cost)
// 获取群聊初始上限
val maxMemberLimit = roomMaxMembers?.defaultMaxTotal ?: 5
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
systemUiController.setNavigationBarColor(Color.Transparent) systemUiController.setNavigationBarColor(Color.Transparent)
} }
@@ -367,33 +389,47 @@ fun CreateGroupChatScreen() {
} }
} }
// Tab切换 // Tab切换和成员数量显示
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.Start, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom verticalAlignment = Alignment.CenterVertically
) { ) {
TabItem( // Tab左对齐
text = stringResource(R.string.chat_ai), Row(
isSelected = pagerState.currentPage == 0, horizontalArrangement = Arrangement.Start,
onClick = { verticalAlignment = Alignment.CenterVertically
scope.launch { ) {
pagerState.animateScrollToPage(0) TabItem(
text = stringResource(R.string.chat_ai),
isSelected = pagerState.currentPage == 0,
onClick = {
scope.launch {
pagerState.animateScrollToPage(0)
}
} }
} )
) TabSpacer()
TabSpacer() TabItem(
TabItem( text = stringResource(R.string.chat_friend),
text = stringResource(R.string.chat_friend), isSelected = pagerState.currentPage == 1,
isSelected = pagerState.currentPage == 1, onClick = {
onClick = { scope.launch {
scope.launch { pagerState.animateScrollToPage(1)
pagerState.animateScrollToPage(1) }
} }
} )
}
// 成员数量显示右对齐x/x格式
Text(
text = "${selectedMembers.size}/$maxMemberLimit",
fontSize = 14.sp,
color = if (selectedMembers.size > maxMemberLimit) AppColors.error else AppColors.secondaryText,
fontWeight = FontWeight.W500
) )
} }
@@ -436,26 +472,60 @@ fun CreateGroupChatScreen() {
} }
} }
// 余额和扣减积分显示(创建按钮上方)
val buttonTopPadding = if (cost > 0) 4.dp else 16.dp
if (cost > 0) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 4.dp),
horizontalArrangement = Arrangement.Center
) {
Text(
text = "${stringResource(R.string.create_group_chat_current_balance)}: ${currentBalance.formatNumber()} ${stringResource(R.string.pai_coin)}",
fontSize = 12.sp,
color = AppColors.secondaryText
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = "${stringResource(R.string.create_group_chat_required_cost)} ${cost.formatNumber()} ${stringResource(R.string.pai_coin)}",
fontSize = 12.sp,
color = AppColors.secondaryText
)
}
}
// 创建群聊按钮 - 固定在底部 // 创建群聊按钮 - 固定在底部
Button( Button(
onClick = { onClick = {
// 创建群聊逻辑 // 创建群聊逻辑
if (selectedMembers.isNotEmpty()) { if (selectedMembers.isNotEmpty()) {
scope.launch { // 检查是否超过上限
val success = CreateGroupChatViewModel.createGroupChat( if (selectedMembers.size > maxMemberLimit) {
groupName = groupName.text, CreateGroupChatViewModel.showError(context.getString(R.string.create_group_chat_exceed_limit, maxMemberLimit))
selectedMembers = selectedMembers, return@Button
context = context }
) // 如果费用大于0显示确认弹窗
if (success) { if (cost > 0) {
navController.popBackStack() CreateGroupChatViewModel.showConfirmDialog()
} else {
// 费用为0直接创建
scope.launch {
val success = CreateGroupChatViewModel.createGroupChat(
groupName = groupName.text,
selectedMembers = selectedMembers,
context = context
)
if (success) {
navController.popBackStack()
}
} }
} }
} }
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = navigationBarPadding + 16.dp), .padding(start = 16.dp, end = 16.dp, top = buttonTopPadding, bottom = navigationBarPadding + 16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = AppColors.main, containerColor = AppColors.main,
contentColor = AppColors.mainText, contentColor = AppColors.mainText,
@@ -482,6 +552,38 @@ fun CreateGroupChatScreen() {
} }
// 消费确认弹窗
if (CreateGroupChatViewModel.showConfirmDialog) {
CreateGroupChatConfirmDialog(
cost = cost,
currentBalance = currentBalance,
balanceAfterCost = balanceAfterCost,
isBalanceSufficient = isBalanceSufficient,
onConfirm = {
// 检查是否超过上限
if (selectedMembers.size > maxMemberLimit) {
CreateGroupChatViewModel.hideConfirmDialog()
CreateGroupChatViewModel.showError(context.getString(R.string.create_group_chat_exceed_limit, maxMemberLimit))
} else {
CreateGroupChatViewModel.hideConfirmDialog()
scope.launch {
val success = CreateGroupChatViewModel.createGroupChat(
groupName = groupName.text,
selectedMembers = selectedMembers,
context = context
)
if (success) {
navController.popBackStack()
}
}
}
},
onCancel = {
CreateGroupChatViewModel.hideConfirmDialog()
}
)
}
// 居中显示的错误提示弹窗 // 居中显示的错误提示弹窗
CreateGroupChatViewModel.errorMessage?.let { error -> CreateGroupChatViewModel.errorMessage?.let { error ->
Box( Box(
@@ -496,7 +598,7 @@ fun CreateGroupChatScreen() {
horizontalAlignment = Alignment.CenterHorizontally, // 水平居中 horizontalAlignment = Alignment.CenterHorizontally, // 水平居中
verticalArrangement = Arrangement.Center // 垂直居中 verticalArrangement = Arrangement.Center // 垂直居中
) { ) {
androidx.compose.material3.Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth(0.8f), .fillMaxWidth(0.8f),
shape = RoundedCornerShape(8.dp) shape = RoundedCornerShape(8.dp)
@@ -508,7 +610,7 @@ fun CreateGroupChatScreen() {
.fillMaxWidth(), .fillMaxWidth(),
color = Color.Red, color = Color.Red,
fontSize = 14.sp, fontSize = 14.sp,
textAlign = androidx.compose.ui.text.style.TextAlign.Center textAlign = TextAlign.Center
) )
} }
} }
@@ -516,3 +618,219 @@ fun CreateGroupChatScreen() {
} }
} }
} }
/**
* 创建群聊消费确认弹窗
*/
@Composable
fun CreateGroupChatConfirmDialog(
cost: Int,
currentBalance: Int,
balanceAfterCost: Int,
isBalanceSufficient: Boolean,
onConfirm: () -> Unit,
onCancel: () -> Unit
) {
val AppColors = LocalAppTheme.current
Dialog(
onDismissRequest = onCancel,
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
)
) {
Card(
modifier = Modifier
.fillMaxWidth(0.9f)
.padding(16.dp),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 硬币图标(使用文本代替,实际项目中可以使用图片资源)
Box(
modifier = Modifier
.size(64.dp)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFFFFD700), // 金色
Color(0xFFFFA500) // 橙色
)
),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Text(
text = "pai",
color = Color.White,
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(16.dp))
// 标题
Text(
text = stringResource(R.string.create_group_chat_confirm_title),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text
)
Spacer(modifier = Modifier.height(24.dp))
// 需要消耗
CostInfoRow(
label = stringResource(R.string.create_group_chat_required_cost),
amount = cost,
AppColors = AppColors
)
Spacer(modifier = Modifier.height(12.dp))
// 当前余额
CostInfoRow(
label = stringResource(R.string.create_group_chat_current_balance),
amount = currentBalance,
AppColors = AppColors
)
Spacer(modifier = Modifier.height(12.dp))
// 消耗后余额
CostInfoRow(
label = stringResource(R.string.create_group_chat_balance_after),
amount = balanceAfterCost,
AppColors = AppColors
)
// 余额不足提示
if (!isBalanceSufficient) {
Spacer(modifier = Modifier.height(12.dp))
Text(
text = stringResource(R.string.create_group_chat_insufficient_balance),
color = Color.Red,
fontSize = 14.sp
)
}
Spacer(modifier = Modifier.height(24.dp))
// 按钮行
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// 取消按钮
OutlinedButton(
onClick = onCancel,
modifier = Modifier
.weight(1f)
.height(48.dp)
.border(
width = 1.dp,
color = AppColors.secondaryText.copy(alpha = 0.3f),
shape = RoundedCornerShape(24.dp)
),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = AppColors.text
),
shape = RoundedCornerShape(24.dp)
) {
Text(
text = stringResource(R.string.cancel),
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
}
// 确认消耗按钮
Button(
onClick = onConfirm,
modifier = Modifier
.weight(1f)
.height(48.dp),
enabled = isBalanceSufficient,
colors = ButtonDefaults.buttonColors(
containerColor = AppColors.main,
contentColor = AppColors.mainText,
disabledContainerColor = AppColors.disabledBackground,
disabledContentColor = AppColors.text
),
shape = RoundedCornerShape(24.dp)
) {
Text(
text = stringResource(R.string.create_group_chat_confirm_consume),
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
}
}
}
}
}
}
/**
* 费用信息行组件
*/
@Composable
fun CostInfoRow(
label: String,
amount: Int,
AppColors: com.aiosman.ravenow.AppThemeData
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = label,
fontSize = 14.sp,
color = AppColors.text
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
// 小硬币图标(使用简单的圆形)
Box(
modifier = Modifier
.size(16.dp)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFFFFD700),
Color(0xFFFFA500)
)
),
shape = CircleShape
)
)
Text(
text = "${amount.formatNumber()} ${stringResource(R.string.pai_coin)}",
fontSize = 14.sp,
fontWeight = FontWeight.W600,
color = AppColors.text
)
}
}
}
/**
* 格式化数字,添加千位分隔符
*/
fun Int.formatNumber(): String {
return this.toString().reversed().chunked(3).joinToString(",").reversed()
}

View File

@@ -16,6 +16,7 @@ import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.data.AccountNotice import com.aiosman.ravenow.data.AccountNotice
import com.aiosman.ravenow.data.AccountService import com.aiosman.ravenow.data.AccountService
import com.aiosman.ravenow.data.AccountServiceImpl import com.aiosman.ravenow.data.AccountServiceImpl
import com.aiosman.ravenow.data.PointService
import com.aiosman.ravenow.data.UserService import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.UserServiceImpl import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
@@ -36,6 +37,7 @@ object CreateGroupChatViewModel : ViewModel() {
// 状态管理 // 状态管理
var isLoading by mutableStateOf(false) var isLoading by mutableStateOf(false)
var errorMessage by mutableStateOf<String?>(null) var errorMessage by mutableStateOf<String?>(null)
var showConfirmDialog by mutableStateOf(false)
// 创建群聊 // 创建群聊
suspend fun createGroupChat( suspend fun createGroupChat(
@@ -76,6 +78,11 @@ object CreateGroupChatViewModel : ViewModel() {
} }
} }
// 显示错误信息(公开方法)
fun showError(message: String) {
showToast(message)
}
// 清除错误信息 // 清除错误信息
fun clearError() { fun clearError() {
errorMessage = null errorMessage = null
@@ -107,4 +114,59 @@ object CreateGroupChatViewModel : ViewModel() {
addSelectedMember(member, selectedMemberIds, selectedMembers) addSelectedMember(member, selectedMemberIds, selectedMembers)
} }
} }
/**
* 获取创建群聊的费用
* @return 费用金额,如果无法获取则返回 0
*/
fun getCreateRoomCost(): Int {
val rules = PointService.pointsRules.value
val costRule = rules?.sub?.get(PointService.PointsRuleKey.CREATE_ROOM)
return when (costRule) {
is PointService.RuleAmount.Fixed -> costRule.value
is PointService.RuleAmount.Range -> costRule.min // 使用最小值作为默认费用
null -> 0
}
}
/**
* 获取当前余额
* @return 当前余额,如果无法获取则返回 0
*/
fun getCurrentBalance(): Int {
return PointService.pointsBalance.value?.balance ?: 0
}
/**
* 计算消耗后余额
* @param cost 费用
* @return 消耗后余额
*/
fun calculateBalanceAfterCost(cost: Int): Int {
val currentBalance = getCurrentBalance()
return (currentBalance - cost).coerceAtLeast(0)
}
/**
* 检查余额是否充足
* @param cost 费用
* @return 是否充足
*/
fun isBalanceSufficient(cost: Int): Boolean {
return getCurrentBalance() >= cost
}
/**
* 显示确认弹窗
*/
fun showConfirmDialog() {
showConfirmDialog = true
}
/**
* 隐藏确认弹窗
*/
fun hideConfirmDialog() {
showConfirmDialog = false
}
} }

View File

@@ -182,6 +182,7 @@ fun Agent() {
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = AppColors.background containerColor = AppColors.background
), ),
windowInsets = WindowInsets(0, 0, 0, 0),
modifier = Modifier modifier = Modifier
.height(44.dp + statusBarPaddingValues.calculateTopPadding()) .height(44.dp + statusBarPaddingValues.calculateTopPadding())
.padding(top = statusBarPaddingValues.calculateTopPadding()) .padding(top = statusBarPaddingValues.calculateTopPadding())

View File

@@ -54,6 +54,8 @@ import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToGroupChat import com.aiosman.ravenow.ui.navigateToGroupChat
import com.aiosman.ravenow.AppStore import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.api.ApiClient
import android.util.Base64
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@@ -209,7 +211,12 @@ fun RoomItem(
val avatarUrl = if (room.avatar.isNotEmpty()) { val avatarUrl = if (room.avatar.isNotEmpty()) {
"${ConstVars.BASE_SERVER}/api/v1/outside/${room.avatar}?token=${AppStore.token}" "${ConstVars.BASE_SERVER}/api/v1/outside/${room.avatar}?token=${AppStore.token}"
} else { } else {
"" // 如果头像为空,使用群头像接口
val groupIdBase64 = Base64.encodeToString(
room.trtcType.toByteArray(),
Base64.NO_WRAP
)
"${ApiClient.RETROFIT_URL}group/avatar?groupIdBase64=${groupIdBase64}&token=${AppStore.token}"
} }
Row( Row(

View File

@@ -74,7 +74,9 @@ import com.aiosman.ravenow.ui.composables.MomentCard
import com.aiosman.ravenow.ui.composables.TabItem import com.aiosman.ravenow.ui.composables.TabItem
import com.aiosman.ravenow.ui.composables.TabSpacer import com.aiosman.ravenow.ui.composables.TabSpacer
import com.aiosman.ravenow.ui.index.tabs.message.tab.AgentChatListViewModel import com.aiosman.ravenow.ui.index.tabs.message.tab.AgentChatListViewModel
import com.aiosman.ravenow.ui.index.tabs.profile.composable.RoomItem
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToGroupChat
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
@@ -88,7 +90,7 @@ fun SearchScreen() {
val context = LocalContext.current val context = LocalContext.current
val model = SearchViewModel val model = SearchViewModel
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val pagerState = rememberPagerState(pageCount = { 3 }) val pagerState = rememberPagerState(pageCount = { 4 })
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues() val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
@@ -168,8 +170,9 @@ fun SearchScreen() {
onClick = { term -> onClick = { term ->
coroutineScope.launch { coroutineScope.launch {
keyboardController?.hide() keyboardController?.hide()
model.searchText = term
model.onTextChanged(term) model.onTextChanged(term)
pagerState.scrollToPage(0) pagerState.animateScrollToPage(0)
model.search() model.search()
} }
}, },
@@ -222,6 +225,18 @@ fun SearchScreen() {
} }
) )
} }
TabSpacer()
Box {
TabItem(
text = stringResource(R.string.chat_group),
isSelected = pagerState.currentPage == 3,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(3)
}
}
)
}
} }
} }
} }
@@ -376,6 +391,7 @@ fun SearchPager(
0 -> MomentResultTab() 0 -> MomentResultTab()
1 -> UserResultTab() 1 -> UserResultTab()
2 -> AiResultTab() 2 -> AiResultTab()
3 -> RoomResultTab()
} }
} }
} }
@@ -753,6 +769,100 @@ fun AiResultTab() {
} }
} }
} }
@Composable
fun RoomResultTab() {
val model = SearchViewModel
val rooms = model.roomsFlow.collectAsLazyPagingItems()
val AppColors = LocalAppTheme.current
val context = LocalContext.current
val navController = LocalNavController.current
Box(
modifier = Modifier
.fillMaxSize()
.background(AppColors.background)
) {
if (rooms.itemCount == 0 && model.showResult) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
androidx.compose.foundation.Image(
painter = painterResource(
id = if (AppState.darkMode) R.mipmap.syss_yh_qs_as_img
else R.mipmap.invalid_name_1
),
contentDescription = "No Result",
modifier = Modifier.size(140.dp)
)
Text(
text = "咦,什么都没找到...",
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "换个关键词试试吧,也许会有新发现!",
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
} else {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ReloadButton(
onClick = {
SearchViewModel.ResetModel()
SearchViewModel.search()
}
)
}
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
items(rooms.itemCount) { idx ->
val roomItem = rooms[idx] ?: return@items
RoomItem(
room = roomItem,
onRoomClick = { roomEntity ->
navController.navigateToGroupChat(
id = roomEntity.trtcRoomId,
name = roomEntity.name,
avatar = roomEntity.avatar
)
}
)
}
}
}
}
}
@Composable @Composable
fun ReloadButton( fun ReloadButton(
onClick: () -> Unit onClick: () -> Unit

View File

@@ -23,6 +23,9 @@ import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.entity.AgentRemoteDataSource import com.aiosman.ravenow.entity.AgentRemoteDataSource
import com.aiosman.ravenow.entity.AgentSearchPagingSource import com.aiosman.ravenow.entity.AgentSearchPagingSource
import com.aiosman.ravenow.entity.AgentServiceImpl import com.aiosman.ravenow.entity.AgentServiceImpl
import com.aiosman.ravenow.entity.RoomEntity
import com.aiosman.ravenow.entity.RoomRemoteDataSource
import com.aiosman.ravenow.entity.RoomSearchPagingSource
import com.aiosman.ravenow.utils.SearchHistoryStore import com.aiosman.ravenow.utils.SearchHistoryStore
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -40,6 +43,8 @@ object SearchViewModel : ViewModel() {
val usersFlow = _usersFlow.asStateFlow() val usersFlow = _usersFlow.asStateFlow()
private val _agentsFlow = MutableStateFlow<PagingData<AgentEntity>>(PagingData.empty()) private val _agentsFlow = MutableStateFlow<PagingData<AgentEntity>>(PagingData.empty())
val agentsFlow = _agentsFlow.asStateFlow() val agentsFlow = _agentsFlow.asStateFlow()
private val _roomsFlow = MutableStateFlow<PagingData<RoomEntity>>(PagingData.empty())
val roomsFlow = _roomsFlow.asStateFlow()
private lateinit var historyStore: SearchHistoryStore private lateinit var historyStore: SearchHistoryStore
private val _historyFlow = MutableStateFlow<List<String>>(emptyList()) private val _historyFlow = MutableStateFlow<List<String>>(emptyList())
val historyFlow = _historyFlow.asStateFlow() val historyFlow = _historyFlow.asStateFlow()
@@ -108,6 +113,19 @@ object SearchViewModel : ViewModel() {
_agentsFlow.value = it _agentsFlow.value = it
} }
} }
viewModelScope.launch {
Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
pagingSourceFactory = {
RoomSearchPagingSource(
RoomRemoteDataSource(),
keyword = searchText
)
}
).flow.cachedIn(viewModelScope).collectLatest {
_roomsFlow.value = it
}
}
showResult = true showResult = true
} }
@@ -153,6 +171,7 @@ object SearchViewModel : ViewModel() {
_momentsFlow.value = PagingData.empty() _momentsFlow.value = PagingData.empty()
_usersFlow.value = PagingData.empty() _usersFlow.value = PagingData.empty()
_agentsFlow.value = PagingData.empty() _agentsFlow.value = PagingData.empty()
_roomsFlow.value = PagingData.empty()
showResult = false showResult = false
} }

View File

@@ -160,6 +160,13 @@ fun EmailSignupScreen() {
) { ) {
StatusBarSpacer() StatusBarSpacer()
// 顶部导航栏:返回箭头 + "注册"标题,左对齐 // 顶部导航栏:返回箭头 + "注册"标题,左对齐
// 根据暗色模式适配颜色
val isDarkMode = AppState.darkMode
val textColor = if (isDarkMode) {
Color.White
} else {
Color.Black
}
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -174,14 +181,14 @@ fun EmailSignupScreen() {
.noRippleClickable { .noRippleClickable {
navController.navigateUp() navController.navigateUp()
}, },
colorFilter = ColorFilter.tint(Color.Black) colorFilter = ColorFilter.tint(textColor)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text( Text(
text = stringResource(R.string.sign_up_upper), text = stringResource(R.string.sign_up_upper),
fontSize = 20.sp, fontSize = 20.sp,
fontWeight = FontWeight.W600, fontWeight = FontWeight.W600,
color = Color.Black color = textColor
) )
} }
@@ -194,6 +201,19 @@ fun EmailSignupScreen() {
) { ) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// 暗色模式下的输入框颜色配置
val isDarkMode = AppState.darkMode
val inputBackgroundColor = if (isDarkMode) {
Color(0xFF1C1C1C) // 暗色模式下深灰色背景
} else {
null // 亮色模式下使用默认背景
}
val textColor = if (isDarkMode) {
Color(0xFFFFFFFF).copy(alpha = 0.7f) // 暗色模式下 label 和 hint 文本颜色
} else {
null // 使用默认颜色
}
// 邮箱输入框 // 邮箱输入框
TextInputField( TextInputField(
modifier = Modifier modifier = Modifier
@@ -214,7 +234,9 @@ fun EmailSignupScreen() {
colorFilter = ColorFilter.tint(IconGray) colorFilter = ColorFilter.tint(IconGray)
) )
}, },
customBackgroundColor = LightGrayBackground, customBackgroundColor = inputBackgroundColor,
customHintColor = textColor,
customLabelColor = textColor,
customCornerRadius = 16f customCornerRadius = 16f
) )
@@ -239,7 +261,9 @@ fun EmailSignupScreen() {
colorFilter = ColorFilter.tint(IconGray) colorFilter = ColorFilter.tint(IconGray)
) )
}, },
customBackgroundColor = LightGrayBackground, customBackgroundColor = inputBackgroundColor,
customHintColor = textColor,
customLabelColor = textColor,
customCornerRadius = 16f customCornerRadius = 16f
) )
@@ -264,7 +288,9 @@ fun EmailSignupScreen() {
colorFilter = ColorFilter.tint(IconGray) colorFilter = ColorFilter.tint(IconGray)
) )
}, },
customBackgroundColor = LightGrayBackground, customBackgroundColor = inputBackgroundColor,
customHintColor = textColor,
customLabelColor = textColor,
customCornerRadius = 16f customCornerRadius = 16f
) )

View File

@@ -280,10 +280,7 @@ fun LoginPage() {
Box( Box(
modifier = Modifier modifier = Modifier
.size(30.dp) .size(30.dp)
.background(
color = AppColors.text.copy(alpha = 0.1f),
shape = androidx.compose.foundation.shape.CircleShape
)
.noRippleClickable { .noRippleClickable {
guestLogin() guestLogin()
}, },
@@ -292,7 +289,7 @@ fun LoginPage() {
Image( Image(
painter = painterResource(id = R.drawable.rider_pro_close), painter = painterResource(id = R.drawable.rider_pro_close),
contentDescription = "Close", contentDescription = "Close",
modifier = Modifier.size(16.dp), modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(AppColors.text) colorFilter = ColorFilter.tint(AppColors.text)
) )
} }
@@ -313,26 +310,26 @@ fun LoginPage() {
.height(400.dp) .height(400.dp)
) { ) {
val lottieFile = if (AppState.darkMode) "login.lottie" else "login_light.lottie" val lottieFile = "login.lottie"
LottieAnimation( LottieAnimation(
composition = rememberLottieComposition(LottieCompositionSpec.Asset(lottieFile)).value, composition = rememberLottieComposition(LottieCompositionSpec.Asset(lottieFile)).value,
iterations = LottieConstants.IterateForever, iterations = LottieConstants.IterateForever,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
} }
Image(
painter = painterResource(if(AppState.darkMode) R.mipmap.login_paipia_dark else R.mipmap.login_paipia_light),
contentDescription = "",
modifier = Modifier
.size(width = 140.dp, height = 30.dp)
)
Spacer(modifier = Modifier.height(10.dp))
Text( Text(
text = stringResource(R.string.join_party_carnival), text = stringResource(R.string.join_party_carnival),
fontSize = 17.sp, fontSize = 17.sp,
color = AppColors.text.copy(alpha = 0.6f),
fontWeight = FontWeight.W600, fontWeight = FontWeight.W600,
color = AppColors.text
) )
// Image(
// painter = painterResource(id = R.mipmap.invalid_name),
// contentDescription = "Rave Now",
// modifier = Modifier
// .size(52.dp)
// .clip(RoundedCornerShape(10.dp))
// )
// Spacer(modifier = Modifier.height(8.dp)) // Spacer(modifier = Modifier.height(8.dp))
// Text( // Text(
// "Rave Now", // "Rave Now",
@@ -347,16 +344,16 @@ fun LoginPage() {
// fontWeight = FontWeight.W700, // fontWeight = FontWeight.W700,
// color = AppColors.text // color = AppColors.text
// ) // )
//注册tab //登录tab
Spacer(modifier = Modifier.height(48.dp)) Spacer(modifier = Modifier.height(35.dp))
ActionButton( ActionButton(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.sign_up_upper), text = stringResource(R.string.login_upper),
color = if (AppState.darkMode) Color.Black else Color.White, color = if (AppState.darkMode) Color.Black else Color.White,
backgroundColor = if (AppState.darkMode) Color.White else Color.Black backgroundColor = if (AppState.darkMode) Color.White else Color.Black
) { ) {
navController.navigate( navController.navigate(
NavigationRoute.EmailSignUp.route, NavigationRoute.UserAuth.route,
) )
} }
//谷歌登录tab //谷歌登录tab
@@ -386,10 +383,10 @@ fun LoginPage() {
googleLogin() googleLogin()
} }
//登录tab //注册tab
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
Text( Text(
text = stringResource(R.string.login_upper), text = stringResource(R.string.sign_up_upper),
color = AppColors.text.copy(alpha = 0.5f), color = AppColors.text.copy(alpha = 0.5f),
fontSize = 16.sp, fontSize = 16.sp,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@@ -397,7 +394,7 @@ fun LoginPage() {
.fillMaxWidth() .fillMaxWidth()
.noRippleClickable { .noRippleClickable {
navController.navigate( navController.navigate(
NavigationRoute.UserAuth.route, NavigationRoute.EmailSignUp.route,
) )
} }
) )

View File

@@ -23,10 +23,11 @@ object PointsViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
try { try {
loading = true loading = true
// 并行预加载积分定价表不影响UI // 并行预加载积分定价表和群聊人数限制不影响UI
launch { launch {
try { try {
PointService.refreshPointsRules() PointService.refreshPointsRules()
PointService.refreshRoomMaxMembers()
} catch (e: Exception) { } catch (e: Exception) {
Log.e("PointsViewModel", "refresh rules error", e) Log.e("PointsViewModel", "refresh rules error", e)
} }

View File

@@ -1,5 +1,6 @@
package com.aiosman.ravenow.ui.profile package com.aiosman.ravenow.ui.profile
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -10,16 +11,21 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -55,7 +61,16 @@ fun AiProfileV3(
onComment: (MomentEntity) -> Unit = {}, onComment: (MomentEntity) -> Unit = {},
) { ) {
val appColors = LocalAppTheme.current val appColors = LocalAppTheme.current
val navController = LocalNavController.current
val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) } val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) }
var showMenu by remember { mutableStateOf(false) }
// 判断是否是创建者
val isCreator = remember(profile) {
profile?.creatorProfile?.id?.let { creatorId ->
AppState.UserId?.toLong() == creatorId
} ?: false
}
Box( Box(
modifier = Modifier modifier = Modifier
@@ -140,12 +155,42 @@ fun AiProfileV3(
} }
// 顶部返回按钮 // 顶部返回按钮
TopBar() TopBar(
onMenuClick = { showMenu = true }
)
}
// 底部菜单
if (showMenu) {
AiProfileMenuModal(
onDismiss = { showMenu = false },
onChatClick = {
showMenu = false
onChatClick()
},
onShareClick = {
showMenu = false
onShareClick()
},
onEditClick = {
showMenu = false
// 导航到编辑页面
profile?.chatAIId?.let { chatAIId ->
navController.navigate(
NavigationRoute.AiPromptEdit.route.replace("{chatAIId}", chatAIId)
)
}
},
profile = profile,
showEdit = isCreator
)
} }
} }
@Composable @Composable
private fun TopBar() { private fun TopBar(
onMenuClick: () -> Unit = {}
) {
val navController = LocalNavController.current val navController = LocalNavController.current
val appColors = LocalAppTheme.current val appColors = LocalAppTheme.current
@@ -182,6 +227,25 @@ private fun TopBar() {
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
} }
// 菜单按钮
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(Color.Black.copy(alpha = 0.3f))
.noRippleClickable {
onMenuClick()
},
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.drawable.rider_pro_more_horizon),
contentDescription = "Menu",
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(Color.White)
)
}
} }
} }
} }
@@ -454,3 +518,130 @@ private fun AiProfileActions(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun AiProfileMenuModal(
onDismiss: () -> Unit,
onChatClick: () -> Unit,
onShareClick: () -> Unit,
onEditClick: () -> Unit,
showEdit: Boolean = false,
profile: AccountProfileEntity? = null
) {
val appColors = LocalAppTheme.current
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
containerColor = appColors.background,
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(if (showEdit) 240.dp else 160.dp)
.background(appColors.background)
.padding(vertical = 47.dp, horizontal = 20.dp)
) {
// 私信选项
Column(
modifier = Modifier
.weight(1f)
.padding(end = 16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.clip(CircleShape)
.noRippleClickable {
onChatClick()
}
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_message),
contentDescription = "",
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(appColors.text)
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.chat_upper),
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = appColors.text
)
}
// 分享选项
Column(
modifier = Modifier
.weight(1f)
.padding(end = if (showEdit) 16.dp else 0.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.clip(CircleShape)
.noRippleClickable {
onShareClick()
}
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_share),
contentDescription = "",
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(appColors.text)
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.share),
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = appColors.text
)
}
// 编辑选项(仅创建者可见)
if (showEdit) {
Column(
modifier = Modifier
.weight(1f),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.clip(CircleShape)
.noRippleClickable {
onEditClick()
}
) {
Image(
painter = painterResource(id = R.drawable.group_info_edit),
contentDescription = "",
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(appColors.text)
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.edit_profile),
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = appColors.text
)
}
}
}
}
}

View File

@@ -44,8 +44,12 @@ class AiProfileViewModel : ViewModel() {
} }
try { try {
profile = userService.getUserProfile(id) // 先通过用户ID获取基本信息获取chatAIId
val basicProfile = userService.getUserProfile(id)
profileId = id.toInt() profileId = id.toInt()
// 使用chatAIId通过getUserProfileByOpenId获取完整信息包含creatorProfile
profile = userService.getUserProfileByOpenId(basicProfile.chatAIId)
} catch (e: Exception) { } catch (e: Exception) {
Log.e("AiProfileViewModel", "Error loading profile", e) Log.e("AiProfileViewModel", "Error loading profile", e)
e.printStackTrace() e.printStackTrace()

View File

@@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- 脑部轮廓 - 更简洁的设计 -->
<path
android:pathData="M12,4C9.2,4 7,6.2 7,9C7,10.2 7.4,11.3 8,12.1C7.6,12.7 7.3,13.4 7.3,14.1C7.3,15.7 8.6,17 10.2,17C10.6,17 11,16.9 11.3,16.7C11.7,17.3 12.4,17.7 13.2,17.7C14.5,17.7 15.5,16.7 15.5,15.4C15.5,15 15.4,14.6 15.2,14.3C15.7,14 16,13.4 16,12.8C16,11.8 15.4,11 14.5,10.7C14.7,10.2 14.8,9.6 14.8,9C14.8,6.2 12.6,4 9.8,4H12Z"
android:strokeLineJoin="round"
android:strokeWidth="1.8"
android:fillColor="#00000000"
android:strokeColor="#7C45ED"
android:fillType="evenOdd"
android:strokeLineCap="round"/>
<!-- 加号 - 位于右上角 -->
<path
android:pathData="M17,7h-1.5v1.5h-1.5v1.5h1.5v1.5h1.5v-1.5h1.5v-1.5h-1.5v-1.5z"
android:fillColor="#7C45ED"
android:fillType="evenOdd"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -367,5 +367,6 @@
<string name="why_not_start_with_agent">エージェントから世界を知り始めませんか?</string> <string name="why_not_start_with_agent">エージェントから世界を知り始めませんか?</string>
<string name="explore">探検する</string> <string name="explore">探検する</string>
<string name="reply_to_user">返信@%1$s</string> <string name="reply_to_user">返信@%1$s</string>
<string name="error_select_at_least_one_image">少なくとも1枚の画像を選択してください。</string>
</resources> </resources>

View File

@@ -368,8 +368,22 @@
<string name="recharge_pai_coin">充值派币</string> <string name="recharge_pai_coin">充值派币</string>
<string name="recharge_pai_coin_desc">多种套餐可选,立即充值</string> <string name="recharge_pai_coin_desc">多种套餐可选,立即充值</string>
<string name="create_group_chat_failed">创建群聊失败: %1$s</string> <string name="create_group_chat_failed">创建群聊失败: %1$s</string>
<string name="create_group_chat_confirm_title">创建群聊确认</string>
<string name="create_group_chat_required_cost">需要消耗:</string>
<string name="create_group_chat_current_balance">当前余额</string>
<string name="create_group_chat_balance_after">消耗后余额:</string>
<string name="create_group_chat_confirm_consume">确认消耗</string>
<string name="create_group_chat_insufficient_balance">余额不足</string>
<string name="create_group_chat_exceed_limit">成员数量超过上限(%1$d</string>
<string name="pai_coin">派币</string>
<string name="cost_required">需要消耗</string>
<string name="balance_after">支付后余额</string>
<string name="insufficient_pai_coin_balance">派币余额不足</string>
<string name="go_recharge">去充值</string>
<string name="confirm_consumption">确认消费</string>
<string name="connect_world_start_following">连接世界,从关注开始</string> <string name="connect_world_start_following">连接世界,从关注开始</string>
<string name="why_not_start_with_agent">不如从一个 Agent 开始认识这世界?</string> <string name="why_not_start_with_agent">不如从一个 Agent 开始认识这世界?</string>
<string name="explore">去探索</string> <string name="explore">去探索</string>
<string name="reply_to_user">回复@%1$s</string> <string name="reply_to_user">回复@%1$s</string>
<string name="error_select_at_least_one_image">请至少选择一张图片</string>
</resources> </resources>

View File

@@ -361,8 +361,22 @@
<string name="recharge_pai_coin">Recharge Pai Coin</string> <string name="recharge_pai_coin">Recharge Pai Coin</string>
<string name="recharge_pai_coin_desc">Multiple packages available, recharge now</string> <string name="recharge_pai_coin_desc">Multiple packages available, recharge now</string>
<string name="create_group_chat_failed">Failed to create group chat: %1$s</string> <string name="create_group_chat_failed">Failed to create group chat: %1$s</string>
<string name="create_group_chat_confirm_title">Create Group Chat</string>
<string name="create_group_chat_required_cost">Required consumption:</string>
<string name="create_group_chat_current_balance">Current balance</string>
<string name="create_group_chat_balance_after">Balance after consumption:</string>
<string name="create_group_chat_confirm_consume">Confirm consumption</string>
<string name="create_group_chat_insufficient_balance">Insufficient balance</string>
<string name="create_group_chat_exceed_limit">Member count exceeds the limit (%1$d)</string>
<string name="pai_coin">Pai Coin</string>
<string name="cost_required">Cost Required</string>
<string name="balance_after">Balance After</string>
<string name="insufficient_pai_coin_balance">Insufficient Pai Coin Balance</string>
<string name="go_recharge">Go Recharge</string>
<string name="confirm_consumption">Confirm Consumption</string>
<string name="connect_world_start_following">Connect the world, start by following</string> <string name="connect_world_start_following">Connect the world, start by following</string>
<string name="why_not_start_with_agent">Why not start exploring the world with an Agent?</string> <string name="why_not_start_with_agent">Why not start exploring the world with an Agent?</string>
<string name="explore">Explore</string> <string name="explore">Explore</string>
<string name="reply_to_user">Reply @%1$s</string> <string name="reply_to_user">Reply @%1$s</string>
<string name="error_select_at_least_one_image">Please select at least one image</string>
</resources> </resources>