@@ -127,5 +127,11 @@ dependencies {
|
||||
implementation (libs.eventbus)
|
||||
implementation(libs.lottie)
|
||||
|
||||
// CameraX + ML Kit(版本在 libs.versions.toml)
|
||||
implementation(libs.androidx.camera.camera2)
|
||||
implementation(libs.androidx.camera.lifecycle)
|
||||
implementation(libs.androidx.camera.view)
|
||||
implementation(libs.mlkit.barcode.scanning)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".RaveNowApplication"
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.aiosman.ravenow.data.AccountService
|
||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||
import com.aiosman.ravenow.data.DictService
|
||||
import com.aiosman.ravenow.data.DictServiceImpl
|
||||
import com.aiosman.ravenow.data.PointService
|
||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||
import com.aiosman.ravenow.ui.favourite.FavouriteListViewModel
|
||||
import com.aiosman.ravenow.ui.favourite.FavouriteNoticeViewModel
|
||||
@@ -81,6 +82,14 @@ object AppState {
|
||||
// 注册 JPush
|
||||
Messaging.registerDevice(scope, context)
|
||||
initChat(context)
|
||||
|
||||
// 设置当前用户并刷新积分信息(完成登录态初始化后)
|
||||
PointService.setCurrentUser(UserId)
|
||||
try {
|
||||
PointService.refreshMyPointsBalance()
|
||||
} catch (e: Exception) {
|
||||
Log.e("AppState", "刷新积分失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,6 +237,8 @@ object AppState {
|
||||
AgentViewModel.ResetModel()
|
||||
MineAgentViewModel.ResetModel()
|
||||
UserId = null
|
||||
// 清空积分全局状态,避免用户切换串号
|
||||
PointService.clear()
|
||||
|
||||
// 清除游客状态
|
||||
AppStore.isGuest = false
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.dialogs.CheckUpdateDialog
|
||||
import com.aiosman.ravenow.ui.navigateToPost
|
||||
import com.aiosman.ravenow.ui.post.NewPostViewModel
|
||||
import com.aiosman.ravenow.ui.points.PointsBottomSheetHost
|
||||
import com.google.firebase.Firebase
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.google.firebase.analytics.analytics
|
||||
@@ -141,6 +142,8 @@ class MainActivity : ComponentActivity() {
|
||||
LocalAppTheme provides AppState.appTheme
|
||||
) {
|
||||
CheckUpdateDialog()
|
||||
// 全局挂载积分底部弹窗 Host
|
||||
PointsBottomSheetHost()
|
||||
Navigation(startDestination) { navController ->
|
||||
|
||||
// 处理带有 postId 的通知点击
|
||||
|
||||
@@ -108,6 +108,15 @@ interface AgentService {
|
||||
authorId: Int? = null
|
||||
): ListContainer<AgentEntity>?
|
||||
|
||||
/**
|
||||
* 根据标题关键字搜索智能体
|
||||
*/
|
||||
suspend fun searchAgentByTitle(
|
||||
pageNumber: Int,
|
||||
pageSize: Int = 20,
|
||||
title: String
|
||||
): ListContainer<AgentEntity>?
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
240
app/src/main/java/com/aiosman/ravenow/data/PointService.kt
Normal file
240
app/src/main/java/com/aiosman/ravenow/data/PointService.kt
Normal file
@@ -0,0 +1,240 @@
|
||||
package com.aiosman.ravenow.data
|
||||
|
||||
import com.aiosman.ravenow.AppStore
|
||||
import com.aiosman.ravenow.data.api.ApiClient
|
||||
import com.aiosman.ravenow.data.api.PointsBalance
|
||||
import com.aiosman.ravenow.data.api.PointsChangeLog
|
||||
import com.aiosman.ravenow.data.api.PointsChangeLogsResponse
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* 积分服务
|
||||
*
|
||||
* 提供积分余额查询和积分变更日志查询功能
|
||||
*/
|
||||
object PointService {
|
||||
|
||||
// 全局可观察的积分余额(仅内存,不落盘)
|
||||
private val _pointsBalance = MutableStateFlow<PointsBalance?>(null)
|
||||
val pointsBalance: StateFlow<PointsBalance?> = _pointsBalance.asStateFlow()
|
||||
|
||||
// 当前已加载的用户ID,用于处理用户切换
|
||||
@Volatile
|
||||
private var currentUserId: Int? = null
|
||||
|
||||
/**
|
||||
* 设置当前用户ID;当用户切换时,会清空旧的积分数据以避免串号
|
||||
*/
|
||||
fun setCurrentUser(userId: Int?) {
|
||||
if (currentUserId != userId) {
|
||||
currentUserId = userId
|
||||
_pointsBalance.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空内存中的积分状态(用于登出或用户切换)
|
||||
*/
|
||||
fun clear() {
|
||||
_pointsBalance.value = null
|
||||
currentUserId = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新当前用户的积分余额(进入应用并完成登录态初始化后调用)
|
||||
* - 若为游客或无 token,则清空并返回
|
||||
*/
|
||||
suspend fun refreshMyPointsBalance(includeStatistics: Boolean = true) {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (AppStore.isGuest || AppStore.token == null) {
|
||||
clear()
|
||||
return@withContext
|
||||
}
|
||||
val balance = getMyPointsBalance(includeStatistics)
|
||||
_pointsBalance.value = balance
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户积分余额
|
||||
*
|
||||
* @param includeStatistics 是否包含统计信息(累计获得和累计消费),默认 true
|
||||
* @return 积分余额信息,包含当前余额和可选的统计数据
|
||||
* @throws Exception 网络请求失败或服务器返回错误
|
||||
*
|
||||
* 示例:
|
||||
* ```kotlin
|
||||
* try {
|
||||
* // 获取包含统计信息的积分余额
|
||||
* val balance = PointService.getMyPointsBalance()
|
||||
* println("当前余额: ${balance.balance}")
|
||||
* println("累计获得: ${balance.totalEarned}")
|
||||
* println("累计消费: ${balance.totalSpent}")
|
||||
*
|
||||
* // 仅获取当前余额
|
||||
* val simpleBalance = PointService.getMyPointsBalance(includeStatistics = false)
|
||||
* println("当前余额: ${simpleBalance.balance}")
|
||||
* } catch (e: Exception) {
|
||||
* println("获取积分余额失败: ${e.message}")
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
suspend fun getMyPointsBalance(includeStatistics: Boolean = true): PointsBalance {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val response = ApiClient.api.getMyPointsBalance(includeStatistics)
|
||||
if (response.isSuccessful) {
|
||||
response.body()?.data ?: throw Exception("响应数据为空")
|
||||
} else {
|
||||
throw Exception("获取积分余额失败: ${response.code()} ${response.message()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户积分变更日志列表
|
||||
*
|
||||
* @param page 页码,默认 1
|
||||
* @param pageSize 每页数量,默认 20
|
||||
* @param changeType 变更类型筛选("add": 增加, "subtract": 减少, "adjust": 调整),null 表示不筛选
|
||||
* @param startTime 开始时间,格式:YYYY-MM-DD,null 表示不限制
|
||||
* @param endTime 结束时间,格式:YYYY-MM-DD,null 表示不限制
|
||||
* @return 积分变更日志列表响应,包含日志列表和分页信息
|
||||
* @throws Exception 网络请求失败或服务器返回错误
|
||||
*
|
||||
* 示例:
|
||||
* ```kotlin
|
||||
* try {
|
||||
* // 获取最近的积分变更日志
|
||||
* val logs = PointService.getMyPointsChangeLogs(page = 1, pageSize = 20)
|
||||
* println("总记录数: ${logs.total}")
|
||||
* logs.list.forEach { log ->
|
||||
* println("${log.createdAt}: ${log.changeType} ${log.amount} (${log.reason})")
|
||||
* }
|
||||
*
|
||||
* // 筛选积分增加记录
|
||||
* val earnLogs = PointService.getMyPointsChangeLogs(changeType = "add")
|
||||
*
|
||||
* // 查询指定时间范围的记录
|
||||
* val rangeLogs = PointService.getMyPointsChangeLogs(
|
||||
* startTime = "2024-01-01",
|
||||
* endTime = "2024-01-31"
|
||||
* )
|
||||
* } catch (e: Exception) {
|
||||
* println("获取积分变更日志失败: ${e.message}")
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
suspend fun getMyPointsChangeLogs(
|
||||
page: Int = 1,
|
||||
pageSize: Int = 20,
|
||||
changeType: String? = null,
|
||||
startTime: String? = null,
|
||||
endTime: String? = null
|
||||
): PointsChangeLogsResponse {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val response = ApiClient.api.getMyPointsChangeLogs(
|
||||
page = page,
|
||||
pageSize = pageSize,
|
||||
changeType = changeType,
|
||||
startTime = startTime,
|
||||
endTime = endTime
|
||||
)
|
||||
if (response.isSuccessful) {
|
||||
response.body() ?: throw Exception("响应数据为空")
|
||||
} else {
|
||||
throw Exception("获取积分变更日志失败: ${response.code()} ${response.message()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 积分变更类型常量
|
||||
*/
|
||||
object ChangeType {
|
||||
/** 积分增加 */
|
||||
const val ADD = "add"
|
||||
/** 积分减少 */
|
||||
const val SUBTRACT = "subtract"
|
||||
/** 积分调整 */
|
||||
const val ADJUST = "adjust"
|
||||
}
|
||||
|
||||
/**
|
||||
* 积分变更原因常量
|
||||
*/
|
||||
object ChangeReason {
|
||||
// 获得积分类型
|
||||
/** 新用户注册奖励 */
|
||||
const val EARN_REGISTER = "earn_register"
|
||||
/** 每日签到奖励 */
|
||||
const val EARN_DAILY = "earn_daily"
|
||||
/** 任务完成奖励 */
|
||||
const val EARN_TASK = "earn_task"
|
||||
/** 邀请好友奖励 */
|
||||
const val EARN_INVITE = "earn_invite"
|
||||
/** 充值获得 */
|
||||
const val EARN_RECHARGE = "earn_recharge"
|
||||
|
||||
// 消费积分类型
|
||||
/** 创建群聊 */
|
||||
const val SPEND_GROUP_CREATE = "spend_group_create"
|
||||
/** 扩容群聊 */
|
||||
const val SPEND_GROUP_EXPAND = "spend_group_expand"
|
||||
/** Agent 私密模式 */
|
||||
const val SPEND_AGENT_PRIVATE = "spend_agent_private"
|
||||
/** Agent 记忆添加 */
|
||||
const val SPEND_AGENT_MEMORY = "spend_agent_memory"
|
||||
/** 房间记忆添加 */
|
||||
const val SPEND_ROOM_MEMORY = "spend_room_memory"
|
||||
/** 自定义聊天背景 */
|
||||
const val SPEND_CHAT_BACKGROUND = "spend_chat_background"
|
||||
/** 定时事件解锁 */
|
||||
const val SPEND_SCHEDULE_EVENT = "spend_schedule_event"
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取变更原因的中文描述
|
||||
*
|
||||
* @param reason 变更原因代码
|
||||
* @return 中文描述
|
||||
*/
|
||||
fun getReasonDescription(reason: String): String {
|
||||
return when (reason) {
|
||||
ChangeReason.EARN_REGISTER -> "新用户注册奖励"
|
||||
ChangeReason.EARN_DAILY -> "每日签到奖励"
|
||||
ChangeReason.EARN_TASK -> "任务完成奖励"
|
||||
ChangeReason.EARN_INVITE -> "邀请好友奖励"
|
||||
ChangeReason.EARN_RECHARGE -> "充值获得"
|
||||
|
||||
ChangeReason.SPEND_GROUP_CREATE -> "创建群聊"
|
||||
ChangeReason.SPEND_GROUP_EXPAND -> "扩容群聊"
|
||||
ChangeReason.SPEND_AGENT_PRIVATE -> "Agent 私密模式"
|
||||
ChangeReason.SPEND_AGENT_MEMORY -> "Agent 记忆添加"
|
||||
ChangeReason.SPEND_ROOM_MEMORY -> "房间记忆添加"
|
||||
ChangeReason.SPEND_CHAT_BACKGROUND -> "自定义聊天背景"
|
||||
ChangeReason.SPEND_SCHEDULE_EVENT -> "定时事件解锁"
|
||||
|
||||
else -> reason // 未知原因,返回原始代码
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取变更类型的中文描述
|
||||
*
|
||||
* @param changeType 变更类型代码
|
||||
* @return 中文描述
|
||||
*/
|
||||
fun getChangeTypeDescription(changeType: String): String {
|
||||
return when (changeType) {
|
||||
ChangeType.ADD -> "增加"
|
||||
ChangeType.SUBTRACT -> "减少"
|
||||
ChangeType.ADJUST -> "调整"
|
||||
else -> changeType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,19 @@ import com.aiosman.ravenow.data.api.UpdateRoomRuleRequestBody
|
||||
import com.aiosman.ravenow.data.api.RoomRuleQuota
|
||||
import com.aiosman.ravenow.data.api.RoomRule
|
||||
import com.aiosman.ravenow.data.api.RoomRuleCreator
|
||||
import com.aiosman.ravenow.entity.AddAgentToRoomFailedItemEntity
|
||||
import com.aiosman.ravenow.entity.AddAgentToRoomItemEntity
|
||||
import com.aiosman.ravenow.entity.AddAgentToRoomResultEntity
|
||||
import com.aiosman.ravenow.entity.AddUserToRoomFailedItemEntity
|
||||
import com.aiosman.ravenow.entity.AddUserToRoomItemEntity
|
||||
import com.aiosman.ravenow.entity.AddUserToRoomResultEntity
|
||||
import com.aiosman.ravenow.entity.CreatorEntity
|
||||
import com.aiosman.ravenow.entity.RemoveAgentFromRoomFailedItemEntity
|
||||
import com.aiosman.ravenow.entity.RemoveAgentFromRoomItemEntity
|
||||
import com.aiosman.ravenow.entity.RemoveAgentFromRoomResultEntity
|
||||
import com.aiosman.ravenow.entity.RemoveUserFromRoomFailedItemEntity
|
||||
import com.aiosman.ravenow.entity.RemoveUserFromRoomItemEntity
|
||||
import com.aiosman.ravenow.entity.RemoveUserFromRoomResultEntity
|
||||
import com.aiosman.ravenow.entity.RoomEntity
|
||||
import com.aiosman.ravenow.entity.RoomRuleEntity
|
||||
import com.aiosman.ravenow.entity.RoomRuleCreatorEntity
|
||||
@@ -173,6 +185,68 @@ interface RoomService {
|
||||
roomId: Int? = null,
|
||||
trtcId: String? = null
|
||||
): RoomRuleQuotaEntity
|
||||
|
||||
// ========== Room Member Management ==========
|
||||
|
||||
/**
|
||||
* 添加用户到房间
|
||||
*
|
||||
* @param roomId 房间ID,与 trtcId 二选一
|
||||
* @param trtcId TRTC群组ID,与 roomId 二选一
|
||||
* @param openIds 要添加的用户OpenID列表
|
||||
* @return 添加结果实体
|
||||
* @throws ServiceException 添加失败时抛出异常
|
||||
*/
|
||||
suspend fun addUserToRoom(
|
||||
roomId: Int? = null,
|
||||
trtcId: String? = null,
|
||||
openIds: List<String>
|
||||
): AddUserToRoomResultEntity
|
||||
|
||||
/**
|
||||
* 添加智能体到房间
|
||||
*
|
||||
* @param roomId 房间ID,与 trtcId 二选一
|
||||
* @param trtcId TRTC群组ID,与 roomId 二选一
|
||||
* @param agentOpenIds 要添加的智能体OpenID列表
|
||||
* @return 添加结果实体
|
||||
* @throws ServiceException 添加失败时抛出异常
|
||||
*/
|
||||
suspend fun addAgentToRoom(
|
||||
roomId: Int? = null,
|
||||
trtcId: String? = null,
|
||||
agentOpenIds: List<String>
|
||||
): AddAgentToRoomResultEntity
|
||||
|
||||
/**
|
||||
* 从房间移除智能体
|
||||
*
|
||||
* @param roomId 房间ID,与 trtcId 二选一
|
||||
* @param trtcId TRTC群组ID,与 roomId 二选一
|
||||
* @param agentOpenIds 要移除的智能体OpenID列表
|
||||
* @return 移除结果实体
|
||||
* @throws ServiceException 移除失败时抛出异常
|
||||
*/
|
||||
suspend fun removeAgentFromRoom(
|
||||
roomId: Int? = null,
|
||||
trtcId: String? = null,
|
||||
agentOpenIds: List<String>
|
||||
): RemoveAgentFromRoomResultEntity
|
||||
|
||||
/**
|
||||
* 从房间移除用户
|
||||
*
|
||||
* @param roomId 房间ID,与 trtcId 二选一
|
||||
* @param trtcId TRTC群组ID,与 roomId 二选一
|
||||
* @param userIds 要移除的用户ID列表(OpenID)
|
||||
* @return 移除结果实体
|
||||
* @throws ServiceException 移除失败时抛出异常
|
||||
*/
|
||||
suspend fun removeUserFromRoom(
|
||||
roomId: Int? = null,
|
||||
trtcId: String? = null,
|
||||
userIds: List<String>
|
||||
): RemoveUserFromRoomResultEntity
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,6 +327,78 @@ class RoomServiceImpl : RoomService {
|
||||
|
||||
return data.toRoomRuleQuotaEntity()
|
||||
}
|
||||
|
||||
override suspend fun addUserToRoom(
|
||||
roomId: Int?,
|
||||
trtcId: String?,
|
||||
openIds: List<String>
|
||||
): AddUserToRoomResultEntity {
|
||||
val resp = ApiClient.api.addUserToRoom(
|
||||
com.aiosman.ravenow.data.api.AddUserToRoomRequestBody(
|
||||
roomId = roomId,
|
||||
trtcId = trtcId,
|
||||
openIds = openIds
|
||||
)
|
||||
)
|
||||
val body = resp.body() ?: throw ServiceException("添加用户到房间失败")
|
||||
val data = body.data ?: throw ServiceException("添加用户响应数据为空")
|
||||
|
||||
return data.result.toAddUserToRoomResultEntity()
|
||||
}
|
||||
|
||||
override suspend fun addAgentToRoom(
|
||||
roomId: Int?,
|
||||
trtcId: String?,
|
||||
agentOpenIds: List<String>
|
||||
): AddAgentToRoomResultEntity {
|
||||
val resp = ApiClient.api.addAgentToRoom(
|
||||
com.aiosman.ravenow.data.api.AddAgentToRoomRequestBody(
|
||||
roomId = roomId,
|
||||
trtcId = trtcId,
|
||||
agentOpenIds = agentOpenIds
|
||||
)
|
||||
)
|
||||
val body = resp.body() ?: throw ServiceException("添加智能体到房间失败")
|
||||
val data = body.data ?: throw ServiceException("添加智能体响应数据为空")
|
||||
|
||||
return data.result.toAddAgentToRoomResultEntity()
|
||||
}
|
||||
|
||||
override suspend fun removeAgentFromRoom(
|
||||
roomId: Int?,
|
||||
trtcId: String?,
|
||||
agentOpenIds: List<String>
|
||||
): RemoveAgentFromRoomResultEntity {
|
||||
val resp = ApiClient.api.removeAgentFromRoom(
|
||||
com.aiosman.ravenow.data.api.RemoveAgentFromRoomRequestBody(
|
||||
roomId = roomId,
|
||||
trtcId = trtcId,
|
||||
agentOpenIds = agentOpenIds
|
||||
)
|
||||
)
|
||||
val body = resp.body() ?: throw ServiceException("从房间移除智能体失败")
|
||||
val data = body.data ?: throw ServiceException("移除智能体响应数据为空")
|
||||
|
||||
return data.toRemoveAgentFromRoomResultEntity()
|
||||
}
|
||||
|
||||
override suspend fun removeUserFromRoom(
|
||||
roomId: Int?,
|
||||
trtcId: String?,
|
||||
userIds: List<String>
|
||||
): RemoveUserFromRoomResultEntity {
|
||||
val resp = ApiClient.api.removeUserFromRoom(
|
||||
com.aiosman.ravenow.data.api.RemoveUserFromRoomRequestBody(
|
||||
roomId = roomId,
|
||||
trtcId = trtcId,
|
||||
userIds = userIds
|
||||
)
|
||||
)
|
||||
val body = resp.body() ?: throw ServiceException("从房间移除用户失败")
|
||||
val data = body.data ?: throw ServiceException("移除用户响应数据为空")
|
||||
|
||||
return data.toRemoveUserFromRoomResultEntity()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,6 +441,128 @@ fun RoomRuleQuota.toRoomRuleQuotaEntity(): RoomRuleQuotaEntity {
|
||||
)
|
||||
}
|
||||
|
||||
// ========== Room Member Management 扩展函数 ==========
|
||||
|
||||
/**
|
||||
* AddUserToRoomResult 扩展函数,转换为 AddUserToRoomResultEntity
|
||||
*/
|
||||
fun com.aiosman.ravenow.data.api.AddUserToRoomResult.toAddUserToRoomResultEntity(): AddUserToRoomResultEntity {
|
||||
return AddUserToRoomResultEntity(
|
||||
totalCount = totalCount,
|
||||
successCount = successCount,
|
||||
failedCount = failedCount,
|
||||
skippedCount = skippedCount,
|
||||
successItems = successItems.map { it.toAddUserToRoomItemEntity() },
|
||||
failedItems = failedItems.map { it.toAddUserToRoomFailedItemEntity() },
|
||||
skippedItems = skippedItems.map { it.toAddUserToRoomItemEntity() }
|
||||
)
|
||||
}
|
||||
|
||||
fun com.aiosman.ravenow.data.api.AddUserToRoomItem.toAddUserToRoomItemEntity(): AddUserToRoomItemEntity {
|
||||
return AddUserToRoomItemEntity(
|
||||
userId = userId,
|
||||
type = type
|
||||
)
|
||||
}
|
||||
|
||||
fun com.aiosman.ravenow.data.api.AddUserToRoomFailedItem.toAddUserToRoomFailedItemEntity(): AddUserToRoomFailedItemEntity {
|
||||
return AddUserToRoomFailedItemEntity(
|
||||
userId = userId,
|
||||
type = type,
|
||||
error = error
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* AddAgentToRoomResult 扩展函数,转换为 AddAgentToRoomResultEntity
|
||||
*/
|
||||
fun com.aiosman.ravenow.data.api.AddAgentToRoomResult.toAddAgentToRoomResultEntity(): AddAgentToRoomResultEntity {
|
||||
return AddAgentToRoomResultEntity(
|
||||
totalCount = totalCount,
|
||||
successCount = successCount,
|
||||
failedCount = failedCount,
|
||||
skippedCount = skippedCount,
|
||||
successItems = successItems.map { it.toAddAgentToRoomItemEntity() },
|
||||
failedItems = failedItems.map { it.toAddAgentToRoomFailedItemEntity() },
|
||||
skippedItems = skippedItems.map { it.toAddAgentToRoomItemEntity() }
|
||||
)
|
||||
}
|
||||
|
||||
fun com.aiosman.ravenow.data.api.AddAgentToRoomItem.toAddAgentToRoomItemEntity(): AddAgentToRoomItemEntity {
|
||||
return AddAgentToRoomItemEntity(
|
||||
agentOpenId = agentOpenId,
|
||||
type = type
|
||||
)
|
||||
}
|
||||
|
||||
fun com.aiosman.ravenow.data.api.AddAgentToRoomFailedItem.toAddAgentToRoomFailedItemEntity(): AddAgentToRoomFailedItemEntity {
|
||||
return AddAgentToRoomFailedItemEntity(
|
||||
agentOpenId = agentOpenId,
|
||||
type = type,
|
||||
error = error
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* RemoveAgentFromRoomResult 扩展函数,转换为 RemoveAgentFromRoomResultEntity
|
||||
*/
|
||||
fun com.aiosman.ravenow.data.api.RemoveAgentFromRoomResult.toRemoveAgentFromRoomResultEntity(): RemoveAgentFromRoomResultEntity {
|
||||
return RemoveAgentFromRoomResultEntity(
|
||||
totalCount = totalCount,
|
||||
successCount = successCount,
|
||||
failedCount = failedCount,
|
||||
skippedCount = skippedCount,
|
||||
successItems = successItems.map { it.toRemoveAgentFromRoomItemEntity() },
|
||||
failedItems = failedItems.map { it.toRemoveAgentFromRoomFailedItemEntity() },
|
||||
skippedItems = skippedItems.map { it.toRemoveAgentFromRoomItemEntity() }
|
||||
)
|
||||
}
|
||||
|
||||
fun com.aiosman.ravenow.data.api.RemoveAgentFromRoomItem.toRemoveAgentFromRoomItemEntity(): RemoveAgentFromRoomItemEntity {
|
||||
return RemoveAgentFromRoomItemEntity(
|
||||
agentOpenId = agentOpenId,
|
||||
type = type
|
||||
)
|
||||
}
|
||||
|
||||
fun com.aiosman.ravenow.data.api.RemoveAgentFromRoomFailedItem.toRemoveAgentFromRoomFailedItemEntity(): RemoveAgentFromRoomFailedItemEntity {
|
||||
return RemoveAgentFromRoomFailedItemEntity(
|
||||
agentOpenId = agentOpenId,
|
||||
type = type,
|
||||
error = error
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* RemoveUserFromRoomResult 扩展函数,转换为 RemoveUserFromRoomResultEntity
|
||||
*/
|
||||
fun com.aiosman.ravenow.data.api.RemoveUserFromRoomResult.toRemoveUserFromRoomResultEntity(): RemoveUserFromRoomResultEntity {
|
||||
return RemoveUserFromRoomResultEntity(
|
||||
totalCount = totalCount,
|
||||
successCount = successCount,
|
||||
failedCount = failedCount,
|
||||
skippedCount = skippedCount,
|
||||
successItems = successItems.map { it.toRemoveUserFromRoomItemEntity() },
|
||||
failedItems = failedItems.map { it.toRemoveUserFromRoomFailedItemEntity() },
|
||||
skippedItems = skippedItems.map { it.toRemoveUserFromRoomItemEntity() }
|
||||
)
|
||||
}
|
||||
|
||||
fun com.aiosman.ravenow.data.api.RemoveUserFromRoomItem.toRemoveUserFromRoomItemEntity(): RemoveUserFromRoomItemEntity {
|
||||
return RemoveUserFromRoomItemEntity(
|
||||
userId = userId,
|
||||
type = type
|
||||
)
|
||||
}
|
||||
|
||||
fun com.aiosman.ravenow.data.api.RemoveUserFromRoomFailedItem.toRemoveUserFromRoomFailedItemEntity(): RemoveUserFromRoomFailedItemEntity {
|
||||
return RemoveUserFromRoomFailedItemEntity(
|
||||
userId = userId,
|
||||
type = type,
|
||||
error = error
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -274,6 +274,260 @@ data class RemoveAccountRequestBody(
|
||||
val password: String,
|
||||
)
|
||||
|
||||
// ========== Room Member Management 相关数据类 ==========
|
||||
|
||||
/**
|
||||
* 添加用户到房间请求体
|
||||
* @param roomId 房间ID(与trtcId互斥,二者必须提供其一)
|
||||
* @param trtcId TRTC群组ID(与roomId互斥,二者必须提供其一)
|
||||
* @param openIds 要添加的用户OpenID列表
|
||||
*/
|
||||
data class AddUserToRoomRequestBody(
|
||||
@SerializedName("roomId")
|
||||
val roomId: Int? = null,
|
||||
@SerializedName("trtcId")
|
||||
val trtcId: String? = null,
|
||||
@SerializedName("openIds")
|
||||
val openIds: List<String>
|
||||
)
|
||||
|
||||
/**
|
||||
* 添加智能体到房间请求体
|
||||
* @param roomId 房间ID(与trtcId互斥,二者必须提供其一)
|
||||
* @param trtcId TRTC群组ID(与roomId互斥,二者必须提供其一)
|
||||
* @param agentOpenIds 要添加的智能体OpenID列表
|
||||
*/
|
||||
data class AddAgentToRoomRequestBody(
|
||||
@SerializedName("roomId")
|
||||
val roomId: Int? = null,
|
||||
@SerializedName("trtcId")
|
||||
val trtcId: String? = null,
|
||||
@SerializedName("agentOpenIds")
|
||||
val agentOpenIds: List<String>
|
||||
)
|
||||
|
||||
/**
|
||||
* 从房间移除智能体请求体
|
||||
* @param roomId 房间ID(与trtcId互斥,二者必须提供其一)
|
||||
* @param trtcId TRTC群组ID(与roomId互斥,二者必须提供其一)
|
||||
* @param agentOpenIds 要移除的智能体OpenID列表
|
||||
*/
|
||||
data class RemoveAgentFromRoomRequestBody(
|
||||
@SerializedName("roomId")
|
||||
val roomId: Int? = null,
|
||||
@SerializedName("trtcId")
|
||||
val trtcId: String? = null,
|
||||
@SerializedName("agentOpenIds")
|
||||
val agentOpenIds: List<String>
|
||||
)
|
||||
|
||||
/**
|
||||
* 从房间移除用户请求体
|
||||
* @param roomId 房间ID(与trtcId互斥,二者必须提供其一)
|
||||
* @param trtcId TRTC群组ID(与roomId互斥,二者必须提供其一)
|
||||
* @param userIds 要移除的用户ID列表(OpenID)
|
||||
*/
|
||||
data class RemoveUserFromRoomRequestBody(
|
||||
@SerializedName("roomId")
|
||||
val roomId: Int? = null,
|
||||
@SerializedName("trtcId")
|
||||
val trtcId: String? = null,
|
||||
@SerializedName("userIds")
|
||||
val userIds: List<String>
|
||||
)
|
||||
|
||||
/**
|
||||
* 添加用户成功项目
|
||||
*/
|
||||
data class AddUserToRoomItem(
|
||||
@SerializedName("userId")
|
||||
val userId: String,
|
||||
@SerializedName("type")
|
||||
val type: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 添加用户失败项目
|
||||
*/
|
||||
data class AddUserToRoomFailedItem(
|
||||
@SerializedName("userId")
|
||||
val userId: String,
|
||||
@SerializedName("type")
|
||||
val type: String,
|
||||
@SerializedName("error")
|
||||
val error: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 添加用户到房间的结果
|
||||
*/
|
||||
data class AddUserToRoomResult(
|
||||
@SerializedName("totalCount")
|
||||
val totalCount: Int,
|
||||
@SerializedName("successCount")
|
||||
val successCount: Int,
|
||||
@SerializedName("failedCount")
|
||||
val failedCount: Int,
|
||||
@SerializedName("skippedCount")
|
||||
val skippedCount: Int,
|
||||
@SerializedName("successItems")
|
||||
val successItems: List<AddUserToRoomItem>,
|
||||
@SerializedName("failedItems")
|
||||
val failedItems: List<AddUserToRoomFailedItem>,
|
||||
@SerializedName("skippedItems")
|
||||
val skippedItems: List<AddUserToRoomItem>
|
||||
)
|
||||
|
||||
/**
|
||||
* 添加用户到房间响应
|
||||
*/
|
||||
data class AddUserToRoomResponse(
|
||||
@SerializedName("message")
|
||||
val message: String,
|
||||
@SerializedName("operationType")
|
||||
val operationType: String,
|
||||
@SerializedName("result")
|
||||
val result: AddUserToRoomResult
|
||||
)
|
||||
|
||||
/**
|
||||
* 添加智能体成功项目
|
||||
*/
|
||||
data class AddAgentToRoomItem(
|
||||
@SerializedName("agentOpenId")
|
||||
val agentOpenId: String,
|
||||
@SerializedName("type")
|
||||
val type: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 添加智能体失败项目
|
||||
*/
|
||||
data class AddAgentToRoomFailedItem(
|
||||
@SerializedName("agentOpenId")
|
||||
val agentOpenId: String,
|
||||
@SerializedName("type")
|
||||
val type: String,
|
||||
@SerializedName("error")
|
||||
val error: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 添加智能体到房间的结果
|
||||
*/
|
||||
data class AddAgentToRoomResult(
|
||||
@SerializedName("totalCount")
|
||||
val totalCount: Int,
|
||||
@SerializedName("successCount")
|
||||
val successCount: Int,
|
||||
@SerializedName("failedCount")
|
||||
val failedCount: Int,
|
||||
@SerializedName("skippedCount")
|
||||
val skippedCount: Int,
|
||||
@SerializedName("successItems")
|
||||
val successItems: List<AddAgentToRoomItem>,
|
||||
@SerializedName("failedItems")
|
||||
val failedItems: List<AddAgentToRoomFailedItem>,
|
||||
@SerializedName("skippedItems")
|
||||
val skippedItems: List<AddAgentToRoomItem>
|
||||
)
|
||||
|
||||
/**
|
||||
* 添加智能体到房间响应
|
||||
*/
|
||||
data class AddAgentToRoomResponse(
|
||||
@SerializedName("message")
|
||||
val message: String,
|
||||
@SerializedName("operationType")
|
||||
val operationType: String,
|
||||
@SerializedName("result")
|
||||
val result: AddAgentToRoomResult
|
||||
)
|
||||
|
||||
/**
|
||||
* 移除智能体成功项目
|
||||
*/
|
||||
data class RemoveAgentFromRoomItem(
|
||||
@SerializedName("agentOpenId")
|
||||
val agentOpenId: String,
|
||||
@SerializedName("type")
|
||||
val type: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 移除智能体失败项目
|
||||
*/
|
||||
data class RemoveAgentFromRoomFailedItem(
|
||||
@SerializedName("agentOpenId")
|
||||
val agentOpenId: String,
|
||||
@SerializedName("type")
|
||||
val type: String,
|
||||
@SerializedName("error")
|
||||
val error: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 从房间移除智能体的结果
|
||||
*/
|
||||
data class RemoveAgentFromRoomResult(
|
||||
@SerializedName("totalCount")
|
||||
val totalCount: Int,
|
||||
@SerializedName("successCount")
|
||||
val successCount: Int,
|
||||
@SerializedName("failedCount")
|
||||
val failedCount: Int,
|
||||
@SerializedName("skippedCount")
|
||||
val skippedCount: Int,
|
||||
@SerializedName("successItems")
|
||||
val successItems: List<RemoveAgentFromRoomItem>,
|
||||
@SerializedName("failedItems")
|
||||
val failedItems: List<RemoveAgentFromRoomFailedItem>,
|
||||
@SerializedName("skippedItems")
|
||||
val skippedItems: List<RemoveAgentFromRoomItem>
|
||||
)
|
||||
|
||||
/**
|
||||
* 移除用户成功项目
|
||||
*/
|
||||
data class RemoveUserFromRoomItem(
|
||||
@SerializedName("userId")
|
||||
val userId: String,
|
||||
@SerializedName("type")
|
||||
val type: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 移除用户失败项目
|
||||
*/
|
||||
data class RemoveUserFromRoomFailedItem(
|
||||
@SerializedName("userId")
|
||||
val userId: String,
|
||||
@SerializedName("type")
|
||||
val type: String,
|
||||
@SerializedName("error")
|
||||
val error: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 从房间移除用户的结果
|
||||
*/
|
||||
data class RemoveUserFromRoomResult(
|
||||
@SerializedName("totalCount")
|
||||
val totalCount: Int,
|
||||
@SerializedName("successCount")
|
||||
val successCount: Int,
|
||||
@SerializedName("failedCount")
|
||||
val failedCount: Int,
|
||||
@SerializedName("skippedCount")
|
||||
val skippedCount: Int,
|
||||
@SerializedName("successItems")
|
||||
val successItems: List<RemoveUserFromRoomItem>,
|
||||
@SerializedName("failedItems")
|
||||
val failedItems: List<RemoveUserFromRoomFailedItem>,
|
||||
@SerializedName("skippedItems")
|
||||
val skippedItems: List<RemoveUserFromRoomItem>
|
||||
)
|
||||
|
||||
// API 错误响应(用于加入房间等接口的错误处理)
|
||||
data class ApiErrorResponse(
|
||||
@SerializedName("err")
|
||||
@@ -762,6 +1016,71 @@ data class InsufficientBalanceError(
|
||||
val traceId: String?
|
||||
)
|
||||
|
||||
// ========== Points 相关数据类 ==========
|
||||
|
||||
/**
|
||||
* 积分余额信息
|
||||
* @param balance 当前积分余额
|
||||
* @param totalEarned 累计获得积分(可选)
|
||||
* @param totalSpent 累计消费积分(可选)
|
||||
*/
|
||||
data class PointsBalance(
|
||||
@SerializedName("balance")
|
||||
val balance: Int,
|
||||
@SerializedName("totalEarned")
|
||||
val totalEarned: Int? = null,
|
||||
@SerializedName("totalSpent")
|
||||
val totalSpent: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* 积分变更日志
|
||||
* @param id 日志记录ID
|
||||
* @param changeType 变更类型(add: 增加, subtract: 减少, adjust: 调整)
|
||||
* @param before 变更前余额
|
||||
* @param after 变更后余额
|
||||
* @param amount 本次变更数量(增加为正数,减少为负数)
|
||||
* @param reason 变更原因代码
|
||||
* @param createdAt 创建时间,格式:YYYY-MM-DD HH:mm:ss
|
||||
*/
|
||||
data class PointsChangeLog(
|
||||
@SerializedName("id")
|
||||
val id: Int,
|
||||
@SerializedName("changeType")
|
||||
val changeType: String,
|
||||
@SerializedName("before")
|
||||
val before: Int,
|
||||
@SerializedName("after")
|
||||
val after: Int,
|
||||
@SerializedName("amount")
|
||||
val amount: Int,
|
||||
@SerializedName("reason")
|
||||
val reason: String,
|
||||
@SerializedName("createdAt")
|
||||
val createdAt: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 积分变更日志列表响应
|
||||
* @param success 请求是否成功
|
||||
* @param list 积分变更日志列表
|
||||
* @param total 总记录数
|
||||
* @param page 当前页码
|
||||
* @param pageSize 每页数量
|
||||
*/
|
||||
data class PointsChangeLogsResponse(
|
||||
@SerializedName("success")
|
||||
val success: Boolean,
|
||||
@SerializedName("list")
|
||||
val list: List<PointsChangeLog>,
|
||||
@SerializedName("total")
|
||||
val total: Int,
|
||||
@SerializedName("page")
|
||||
val page: Int,
|
||||
@SerializedName("pageSize")
|
||||
val pageSize: Int
|
||||
)
|
||||
|
||||
interface RaveNowAPI {
|
||||
@GET("membership/config")
|
||||
@retrofit2.http.Headers("X-Requires-Auth: true")
|
||||
@@ -1605,5 +1924,209 @@ interface RaveNowAPI {
|
||||
@Query("promptReplaceTrans") promptReplaceTrans: Boolean? = null
|
||||
): Response<RecommendationsResponse>
|
||||
|
||||
// ========== Points API ==========
|
||||
|
||||
/**
|
||||
* 获取我的积分余额
|
||||
*
|
||||
* 功能说明:
|
||||
* - 获取当前登录用户的积分余额
|
||||
* - 可选返回累计获得和累计消费统计信息
|
||||
*
|
||||
* @param includeStatistics 是否包含统计信息(累计获得和累计消费),默认 true
|
||||
*
|
||||
* @return 返回积分余额和统计信息
|
||||
*
|
||||
* 响应数据说明:
|
||||
* - balance: 当前积分余额
|
||||
* - totalEarned: 累计获得积分(仅当 includeStatistics 为 true 时返回)
|
||||
* - totalSpent: 累计消费积分(仅当 includeStatistics 为 true 时返回)
|
||||
*
|
||||
* 示例:
|
||||
* ```kotlin
|
||||
* // 获取包含统计信息的积分余额
|
||||
* val response1 = api.getMyPointsBalance()
|
||||
*
|
||||
* // 仅获取当前余额
|
||||
* val response2 = api.getMyPointsBalance(includeStatistics = false)
|
||||
* ```
|
||||
*/
|
||||
@GET("account/my/points")
|
||||
suspend fun getMyPointsBalance(
|
||||
@Query("includeStatistics") includeStatistics: Boolean? = null
|
||||
): Response<DataContainer<PointsBalance>>
|
||||
|
||||
/**
|
||||
* 获取我的积分变更日志
|
||||
*
|
||||
* 功能说明:
|
||||
* - 获取当前登录用户的积分变更日志列表
|
||||
* - 支持分页、时间范围筛选和变更类型筛选
|
||||
*
|
||||
* @param page 页码,默认 1
|
||||
* @param pageSize 每页数量,默认 20
|
||||
* @param changeType 变更类型筛选(add: 增加, subtract: 减少, adjust: 调整)
|
||||
* @param startTime 开始时间,格式:YYYY-MM-DD
|
||||
* @param endTime 结束时间,格式:YYYY-MM-DD
|
||||
*
|
||||
* @return 返回分页的积分变更日志列表
|
||||
*
|
||||
* 响应数据说明:
|
||||
* - list: 积分变更日志列表
|
||||
* - total: 总记录数
|
||||
* - page: 当前页码
|
||||
* - pageSize: 每页数量
|
||||
*
|
||||
* 示例:
|
||||
* ```kotlin
|
||||
* // 获取最近的积分变更日志
|
||||
* val response1 = api.getMyPointsChangeLogs(page = 1, pageSize = 20)
|
||||
*
|
||||
* // 筛选积分增加记录
|
||||
* val response2 = api.getMyPointsChangeLogs(changeType = "add")
|
||||
*
|
||||
* // 查询指定时间范围的记录
|
||||
* val response3 = api.getMyPointsChangeLogs(
|
||||
* startTime = "2024-01-01",
|
||||
* endTime = "2024-01-31"
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
@GET("account/my/points/logs")
|
||||
suspend fun getMyPointsChangeLogs(
|
||||
@Query("page") page: Int = 1,
|
||||
@Query("pageSize") pageSize: Int = 20,
|
||||
@Query("changeType") changeType: String? = null,
|
||||
@Query("startTime") startTime: String? = null,
|
||||
@Query("endTime") endTime: String? = null
|
||||
): Response<PointsChangeLogsResponse>
|
||||
|
||||
// ========== Room Member Management API ==========
|
||||
|
||||
/**
|
||||
* 添加用户到房间
|
||||
*
|
||||
* 功能说明:
|
||||
* - 向房间批量添加用户
|
||||
* - 支持通过房间ID或TRTC群组ID添加
|
||||
* - 只有房间创建者可以使用此接口
|
||||
* - 添加数量受房间容量限制
|
||||
* - 成功添加后会自动扣除房间创建者的积分用于扩容
|
||||
*
|
||||
* @param body 添加用户请求体
|
||||
* - roomId: 房间ID(与 trtcId 二选一)
|
||||
* - trtcId: TRTC群组ID(与 roomId 二选一)
|
||||
* - openIds: 要添加的用户OpenID列表
|
||||
*
|
||||
* @return 成功时返回操作结果,包含成功、失败、跳过的用户列表
|
||||
*
|
||||
* 示例:
|
||||
* ```kotlin
|
||||
* val request = AddUserToRoomRequestBody(
|
||||
* roomId = 123,
|
||||
* openIds = listOf("user_openid_1", "user_openid_2")
|
||||
* )
|
||||
* val response = api.addUserToRoom(request)
|
||||
* ```
|
||||
*/
|
||||
@POST("outside/rooms/add-user")
|
||||
suspend fun addUserToRoom(
|
||||
@Body body: AddUserToRoomRequestBody
|
||||
): Response<DataContainer<AddUserToRoomResponse>>
|
||||
|
||||
/**
|
||||
* 添加智能体到房间
|
||||
*
|
||||
* 功能说明:
|
||||
* - 向房间批量添加智能体(Agent)
|
||||
* - 支持通过房间ID或TRTC群组ID添加
|
||||
* - 房间创建者可以使用此接口
|
||||
* - 当容量不足时会自动扩容并扣除房间创建者的积分
|
||||
* - 如果积分不足以支付扩容费用,将返回错误
|
||||
*
|
||||
* @param body 添加智能体请求体
|
||||
* - roomId: 房间ID(与 trtcId 二选一)
|
||||
* - trtcId: TRTC群组ID(与 roomId 二选一)
|
||||
* - agentOpenIds: 要添加的智能体OpenID列表
|
||||
*
|
||||
* @return 成功时返回操作结果,包含成功、失败、跳过的智能体列表
|
||||
*
|
||||
* 示例:
|
||||
* ```kotlin
|
||||
* val request = AddAgentToRoomRequestBody(
|
||||
* roomId = 123,
|
||||
* agentOpenIds = listOf("agent_openid_1", "agent_openid_2")
|
||||
* )
|
||||
* val response = api.addAgentToRoom(request)
|
||||
* ```
|
||||
*/
|
||||
@POST("outside/rooms/add-agent")
|
||||
suspend fun addAgentToRoom(
|
||||
@Body body: AddAgentToRoomRequestBody
|
||||
): Response<DataContainer<AddAgentToRoomResponse>>
|
||||
|
||||
/**
|
||||
* 从房间移除智能体
|
||||
*
|
||||
* 功能说明:
|
||||
* - 从房间批量移除智能体(Agent)
|
||||
* - 支持通过房间ID或TRTC群组ID操作
|
||||
* - 只有房间创建者可以使用此接口
|
||||
* - 单聊房间不支持移除智能体
|
||||
* - 移除操作会同步到OpenIM系统
|
||||
*
|
||||
* @param body 移除智能体请求体
|
||||
* - roomId: 房间ID(与 trtcId 二选一)
|
||||
* - trtcId: TRTC群组ID(与 roomId 二选一)
|
||||
* - agentOpenIds: 要移除的智能体OpenID列表
|
||||
*
|
||||
* @return 成功时返回操作结果,包含成功、失败、跳过的智能体列表
|
||||
*
|
||||
* 示例:
|
||||
* ```kotlin
|
||||
* val request = RemoveAgentFromRoomRequestBody(
|
||||
* roomId = 123,
|
||||
* agentOpenIds = listOf("agent_openid_1", "agent_openid_2")
|
||||
* )
|
||||
* val response = api.removeAgentFromRoom(request)
|
||||
* ```
|
||||
*/
|
||||
@POST("outside/rooms/remove-agent")
|
||||
suspend fun removeAgentFromRoom(
|
||||
@Body body: RemoveAgentFromRoomRequestBody
|
||||
): Response<DataContainer<RemoveAgentFromRoomResult>>
|
||||
|
||||
/**
|
||||
* 从房间移除用户
|
||||
*
|
||||
* 功能说明:
|
||||
* - 从房间批量移除用户
|
||||
* - 支持通过房间ID或TRTC群组ID操作
|
||||
* - 只有房间创建者可以使用此接口
|
||||
* - 单聊房间不支持移除用户
|
||||
* - 群主不能移除自己
|
||||
* - 移除操作会同步到OpenIM系统
|
||||
*
|
||||
* @param body 移除用户请求体
|
||||
* - roomId: 房间ID(与 trtcId 二选一)
|
||||
* - trtcId: TRTC群组ID(与 roomId 二选一)
|
||||
* - userIds: 要移除的用户ID列表(OpenID)
|
||||
*
|
||||
* @return 成功时返回操作结果,包含成功、失败、跳过的用户列表
|
||||
*
|
||||
* 示例:
|
||||
* ```kotlin
|
||||
* val request = RemoveUserFromRoomRequestBody(
|
||||
* roomId = 123,
|
||||
* userIds = listOf("user_openid_1", "user_openid_2")
|
||||
* )
|
||||
* val response = api.removeUserFromRoom(request)
|
||||
* ```
|
||||
*/
|
||||
@POST("outside/rooms/remove-user")
|
||||
suspend fun removeUserFromRoom(
|
||||
@Body body: RemoveUserFromRoomRequestBody
|
||||
): Response<DataContainer<RemoveUserFromRoomResult>>
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,35 @@ class AgentPagingSource(
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能体搜索分页加载器(按标题关键字)
|
||||
*/
|
||||
class AgentSearchPagingSource(
|
||||
private val agentRemoteDataSource: AgentRemoteDataSource,
|
||||
private val keyword: String,
|
||||
) : PagingSource<Int, AgentEntity>() {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, AgentEntity> {
|
||||
return try {
|
||||
val currentPage = params.key ?: 1
|
||||
val agents = agentRemoteDataSource.searchAgentByTitle(
|
||||
pageNumber = currentPage,
|
||||
title = keyword
|
||||
)
|
||||
LoadResult.Page(
|
||||
data = agents?.list ?: listOf(),
|
||||
prevKey = if (currentPage == 1) null else currentPage - 1,
|
||||
nextKey = if (agents?.list?.isNotEmpty() == true) currentPage + 1 else null
|
||||
)
|
||||
} catch (exception: IOException) {
|
||||
LoadResult.Error(exception)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, AgentEntity>): Int? {
|
||||
return state.anchorPosition
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AgentRemoteDataSource(
|
||||
private val agentService: AgentService,
|
||||
@@ -103,6 +132,16 @@ class AgentRemoteDataSource(
|
||||
authorId = authorId
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun searchAgentByTitle(
|
||||
pageNumber: Int,
|
||||
title: String
|
||||
): ListContainer<AgentEntity>? {
|
||||
return agentService.searchAgentByTitle(
|
||||
pageNumber = pageNumber,
|
||||
title = title
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class AgentServiceImpl() : AgentService {
|
||||
@@ -118,6 +157,17 @@ class AgentServiceImpl() : AgentService {
|
||||
authorId = authorId
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun searchAgentByTitle(
|
||||
pageNumber: Int,
|
||||
pageSize: Int,
|
||||
title: String
|
||||
): ListContainer<AgentEntity>? {
|
||||
return agentBackend.searchAgentByTitle(
|
||||
pageNumber = pageNumber,
|
||||
title = title
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class AgentBackend {
|
||||
@@ -175,6 +225,27 @@ class AgentBackend {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun searchAgentByTitle(
|
||||
pageNumber: Int,
|
||||
title: String
|
||||
): ListContainer<AgentEntity>? {
|
||||
val resp = ApiClient.api.getAgent(
|
||||
page = pageNumber,
|
||||
pageSize = DataBatchSize,
|
||||
withWorkflow = 1,
|
||||
title = title
|
||||
)
|
||||
val body = resp.body() ?: return null
|
||||
val dataContainer = body as DataContainer<ListContainer<Agent>>
|
||||
val listContainer = dataContainer.data
|
||||
return ListContainer(
|
||||
total = listContainer.total,
|
||||
page = pageNumber,
|
||||
pageSize = DataBatchSize,
|
||||
list = listContainer.list.map { it.toAgentEntity() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class AgentEntity(
|
||||
|
||||
@@ -86,6 +86,128 @@ data class RoomRuleQuotaEntity(
|
||||
val usagePercent: Double
|
||||
)
|
||||
|
||||
// ========== Room Member Management 实体类 ==========
|
||||
|
||||
/**
|
||||
* 添加用户成功项目
|
||||
*/
|
||||
data class AddUserToRoomItemEntity(
|
||||
val userId: String,
|
||||
val type: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 添加用户失败项目
|
||||
*/
|
||||
data class AddUserToRoomFailedItemEntity(
|
||||
val userId: String,
|
||||
val type: String,
|
||||
val error: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 添加用户到房间的结果
|
||||
*/
|
||||
data class AddUserToRoomResultEntity(
|
||||
val totalCount: Int,
|
||||
val successCount: Int,
|
||||
val failedCount: Int,
|
||||
val skippedCount: Int,
|
||||
val successItems: List<AddUserToRoomItemEntity>,
|
||||
val failedItems: List<AddUserToRoomFailedItemEntity>,
|
||||
val skippedItems: List<AddUserToRoomItemEntity>
|
||||
)
|
||||
|
||||
/**
|
||||
* 添加智能体成功项目
|
||||
*/
|
||||
data class AddAgentToRoomItemEntity(
|
||||
val agentOpenId: String,
|
||||
val type: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 添加智能体失败项目
|
||||
*/
|
||||
data class AddAgentToRoomFailedItemEntity(
|
||||
val agentOpenId: String,
|
||||
val type: String,
|
||||
val error: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 添加智能体到房间的结果
|
||||
*/
|
||||
data class AddAgentToRoomResultEntity(
|
||||
val totalCount: Int,
|
||||
val successCount: Int,
|
||||
val failedCount: Int,
|
||||
val skippedCount: Int,
|
||||
val successItems: List<AddAgentToRoomItemEntity>,
|
||||
val failedItems: List<AddAgentToRoomFailedItemEntity>,
|
||||
val skippedItems: List<AddAgentToRoomItemEntity>
|
||||
)
|
||||
|
||||
/**
|
||||
* 移除智能体成功项目
|
||||
*/
|
||||
data class RemoveAgentFromRoomItemEntity(
|
||||
val agentOpenId: String,
|
||||
val type: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 移除智能体失败项目
|
||||
*/
|
||||
data class RemoveAgentFromRoomFailedItemEntity(
|
||||
val agentOpenId: String,
|
||||
val type: String,
|
||||
val error: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 从房间移除智能体的结果
|
||||
*/
|
||||
data class RemoveAgentFromRoomResultEntity(
|
||||
val totalCount: Int,
|
||||
val successCount: Int,
|
||||
val failedCount: Int,
|
||||
val skippedCount: Int,
|
||||
val successItems: List<RemoveAgentFromRoomItemEntity>,
|
||||
val failedItems: List<RemoveAgentFromRoomFailedItemEntity>,
|
||||
val skippedItems: List<RemoveAgentFromRoomItemEntity>
|
||||
)
|
||||
|
||||
/**
|
||||
* 移除用户成功项目
|
||||
*/
|
||||
data class RemoveUserFromRoomItemEntity(
|
||||
val userId: String,
|
||||
val type: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 移除用户失败项目
|
||||
*/
|
||||
data class RemoveUserFromRoomFailedItemEntity(
|
||||
val userId: String,
|
||||
val type: String,
|
||||
val error: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 从房间移除用户的结果
|
||||
*/
|
||||
data class RemoveUserFromRoomResultEntity(
|
||||
val totalCount: Int,
|
||||
val successCount: Int,
|
||||
val failedCount: Int,
|
||||
val skippedCount: Int,
|
||||
val successItems: List<RemoveUserFromRoomItemEntity>,
|
||||
val failedItems: List<RemoveUserFromRoomFailedItemEntity>,
|
||||
val skippedItems: List<RemoveUserFromRoomItemEntity>
|
||||
)
|
||||
|
||||
class RoomLoader : DataLoader<AgentEntity,AgentLoaderExtraArgs>() {
|
||||
override suspend fun fetchData(
|
||||
page: Int,
|
||||
|
||||
@@ -78,6 +78,7 @@ import com.aiosman.ravenow.ui.post.PostScreen
|
||||
import com.aiosman.ravenow.ui.profile.AccountProfileV2
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.vip.VipSelPage
|
||||
import com.aiosman.ravenow.ui.notification.NotificationScreen
|
||||
import com.aiosman.ravenow.ui.scan.ScanQrScreen
|
||||
|
||||
sealed class NavigationRoute(
|
||||
val route: String,
|
||||
@@ -130,6 +131,7 @@ sealed class NavigationRoute(
|
||||
data object NotificationScreen : NavigationRoute("NotificationScreen")
|
||||
data object MbtiSelect : NavigationRoute("MbtiSelect")
|
||||
data object ZodiacSelect : NavigationRoute("ZodiacSelect")
|
||||
data object ScanQr : NavigationRoute("ScanQr")
|
||||
}
|
||||
|
||||
|
||||
@@ -453,6 +455,9 @@ fun NavigationController(
|
||||
SearchScreen()
|
||||
}
|
||||
}
|
||||
composable(route = NavigationRoute.ScanQr.route) {
|
||||
ScanQrScreen()
|
||||
}
|
||||
composable(
|
||||
route = NavigationRoute.FollowerList.route,
|
||||
arguments = listOf(navArgument("id") { type = NavType.IntType })
|
||||
|
||||
@@ -586,7 +586,11 @@ fun SideMenuContent(
|
||||
.align(Alignment.TopEnd)
|
||||
.offset(x = (-112).dp, y = 88.dp)
|
||||
.noRippleClickable {
|
||||
// TODO: 实现扫一扫功能
|
||||
// 扫一扫功能:跳转到扫码页面
|
||||
coroutineScope.launch {
|
||||
onClose()
|
||||
navController.navigate(NavigationRoute.ScanQr.route)
|
||||
}
|
||||
},
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
||||
@@ -88,6 +88,10 @@ import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.compose.foundation.lazy.grid.items as gridItems
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
|
||||
// 检测是否接近列表底部的扩展函数
|
||||
fun LazyListState.isScrolledToEnd(buffer: Int = 3): Boolean {
|
||||
@@ -273,48 +277,50 @@ fun Agent() {
|
||||
}
|
||||
}
|
||||
|
||||
// 热门聊天室
|
||||
stickyHeader(key = "hot_rooms_header") {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(AppColors.background)
|
||||
.padding(top = 8.dp, bottom = 12.dp),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.mipmap.rider_pro_hot_room),
|
||||
contentDescription = "chat room",
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
androidx.compose.material3.Text(
|
||||
text = stringResource(R.string.hot_rooms),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
|
||||
color = AppColors.text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 热门聊天室网格
|
||||
items(viewModel.chatRooms.chunked(2)) { rowRooms ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
rowRooms.forEach { chatRoom ->
|
||||
ChatRoomCard(
|
||||
chatRoom = chatRoom,
|
||||
navController = LocalNavController.current,
|
||||
modifier = Modifier.weight(1f)
|
||||
if (viewModel.chatRooms.isNotEmpty()) {
|
||||
// 热门聊天室
|
||||
stickyHeader(key = "hot_rooms_header") {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(AppColors.background)
|
||||
.padding(top = 8.dp, bottom = 12.dp),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.mipmap.rider_pro_hot_room),
|
||||
contentDescription = "chat room",
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
androidx.compose.material3.Text(
|
||||
text = stringResource(R.string.hot_rooms),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
|
||||
color = AppColors.text
|
||||
)
|
||||
}
|
||||
if (rowRooms.size == 1) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
// 热门聊天室网格
|
||||
items(viewModel.chatRooms.chunked(2)) { rowRooms ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
rowRooms.forEach { chatRoom ->
|
||||
ChatRoomCard(
|
||||
chatRoom = chatRoom,
|
||||
navController = LocalNavController.current,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
if (rowRooms.size == 1) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -581,73 +587,147 @@ fun AgentCardSquare(
|
||||
@Composable
|
||||
fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
if (agentItems.isEmpty()) return
|
||||
|
||||
// 每页显示5个agent
|
||||
val itemsPerPage = 5
|
||||
val totalPages = (agentItems.size + itemsPerPage - 1) / itemsPerPage
|
||||
val pagerState = rememberPagerState(pageCount = { agentItems.size })
|
||||
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
|
||||
val cardAspect = 1133.5f / 846.4f
|
||||
// 外层 LazyColumn 左右各 8dp + Pager contentPadding 左右各 20dp
|
||||
val horizontalPaddings = 56.dp
|
||||
val pagerHeight = (screenWidth - horizontalPaddings) * cardAspect
|
||||
|
||||
if (totalPages > 0) {
|
||||
val pagerState = rememberPagerState(pageCount = { totalPages })
|
||||
Column {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(pagerHeight)
|
||||
) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 20.dp),
|
||||
pageSpacing = 12.dp
|
||||
) { page ->
|
||||
// 缩放效果
|
||||
val pageOffset = (
|
||||
(pagerState.currentPage - page) + pagerState
|
||||
.currentPageOffsetFraction
|
||||
).coerceIn(-1f, 1f)
|
||||
val scale = 1f - (0.06f * kotlin.math.abs(pageOffset))
|
||||
|
||||
Column {
|
||||
// Agent内容
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(310.dp)
|
||||
) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 4.dp),
|
||||
pageSpacing = 0.dp
|
||||
) { page ->
|
||||
// 计算当前页面的偏移量
|
||||
val pageOffset = (
|
||||
(pagerState.currentPage - page) + pagerState
|
||||
.currentPageOffsetFraction
|
||||
).coerceIn(-1f, 1f)
|
||||
AgentLargeCard(
|
||||
agentItem = agentItems[page],
|
||||
viewModel = viewModel,
|
||||
navController = LocalNavController.current,
|
||||
modifier = Modifier
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据偏移量计算缩放比例
|
||||
val scale = 1f - (0.1f * kotlin.math.abs(pageOffset))
|
||||
@SuppressLint("SuspiciousIndentation")
|
||||
@Composable
|
||||
fun AgentLargeCard(
|
||||
agentItem: AgentItem,
|
||||
viewModel: AgentViewModel,
|
||||
navController: NavHostController,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
var lastClickTime by remember { mutableStateOf(0L) }
|
||||
|
||||
AgentPage(
|
||||
viewModel = viewModel,
|
||||
agentItems = agentItems.drop(page * itemsPerPage).take(itemsPerPage),
|
||||
page = page,
|
||||
modifier = Modifier
|
||||
.height(310.dp)
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
},
|
||||
navController = LocalNavController.current,
|
||||
)
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(846.4f / 1133.5f)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.noRippleClickable {
|
||||
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
||||
viewModel.goToProfile(agentItem.openId, navController)
|
||||
}) {
|
||||
lastClickTime = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
) {
|
||||
// 背景大图
|
||||
CustomAsyncImage(
|
||||
imageUrl = agentItem.avatar,
|
||||
contentDescription = agentItem.title,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = androidx.compose.ui.layout.ContentScale.Crop
|
||||
)
|
||||
|
||||
// 指示器
|
||||
Row(
|
||||
// 底部渐变与文字
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
0f to Color.Transparent,
|
||||
1f to Color(0xB2000000)
|
||||
)
|
||||
)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(30.dp)
|
||||
.padding(top = 12.dp),
|
||||
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.Center
|
||||
.padding(bottom = 56.dp) // 为底部聊天按钮预留空间
|
||||
) {
|
||||
repeat(totalPages) { index ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.size(3.dp)
|
||||
.background(
|
||||
color = if (pagerState.currentPage == index) AppColors.text else AppColors.secondaryText.copy(
|
||||
alpha = 0.3f
|
||||
),
|
||||
shape = androidx.compose.foundation.shape.CircleShape
|
||||
)
|
||||
)
|
||||
}
|
||||
androidx.compose.material3.Text(
|
||||
text = agentItem.title,
|
||||
color = Color.White,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W700,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
androidx.compose.material3.Text(
|
||||
text = agentItem.desc,
|
||||
color = Color.White.copy(alpha = 0.92f),
|
||||
fontSize = 14.sp,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 底部居中 Chat 按钮
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 16.dp)
|
||||
.widthIn(min = 180.dp)
|
||||
.fillMaxWidth(0.65f)
|
||||
.height(44.dp)
|
||||
.background(AppColors.text, RoundedCornerShape(22.dp))
|
||||
.noRippleClickable {
|
||||
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
} else {
|
||||
viewModel.createSingleChat(agentItem.openId)
|
||||
viewModel.goToChatAi(agentItem.openId, navController)
|
||||
}
|
||||
}) {
|
||||
lastClickTime = System.currentTimeMillis()
|
||||
}
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
androidx.compose.material3.Text(
|
||||
text = stringResource(R.string.chat),
|
||||
color = AppColors.background,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W600
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.aiosman.ravenow.event.MomentAddEvent
|
||||
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
|
||||
import com.aiosman.ravenow.event.MomentLikeChangeEvent
|
||||
import com.aiosman.ravenow.event.MomentRemoveEvent
|
||||
import com.aiosman.ravenow.data.PointService
|
||||
import kotlinx.coroutines.launch
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
@@ -76,6 +77,14 @@ object MyProfileViewModel : ViewModel() {
|
||||
}
|
||||
firstLoad = false
|
||||
loadUserProfile()
|
||||
// 刷新积分(仅非游客)
|
||||
if (!AppStore.isGuest) {
|
||||
try {
|
||||
PointService.refreshMyPointsBalance()
|
||||
} catch (e: Exception) {
|
||||
Log.e("MyProfileViewModel", "refresh points error: ", e)
|
||||
}
|
||||
}
|
||||
refreshing = false
|
||||
|
||||
// 游客模式下不加载个人动态和智能体
|
||||
|
||||
@@ -62,6 +62,7 @@ import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -74,6 +75,7 @@ import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.MainActivity
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.data.PointService
|
||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||
import com.aiosman.ravenow.entity.AgentEntity
|
||||
import com.aiosman.ravenow.entity.MomentEntity
|
||||
@@ -108,6 +110,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
import com.aiosman.ravenow.ui.points.PointsBottomSheet
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -165,6 +168,7 @@ fun ProfileV3(
|
||||
val listState = rememberLazyListState()
|
||||
val gridState = rememberLazyGridState()
|
||||
val scrollState = rememberScrollState()
|
||||
val pointsBalanceState = PointService.pointsBalance.collectAsState(initial = null)
|
||||
|
||||
// 计算导航栏背景透明度,根据滚动位置从0到1
|
||||
val toolbarBackgroundAlpha by remember {
|
||||
@@ -506,6 +510,9 @@ fun ProfileV3(
|
||||
navController = navController,
|
||||
backgroundAlpha = toolbarBackgroundAlpha,
|
||||
interactionCount = moments.sumOf { it.likeCount }, // 计算总点赞数作为互动数据
|
||||
onPointsClick = {
|
||||
if (isSelf) com.aiosman.ravenow.ui.points.PointsSheetManager.open()
|
||||
},
|
||||
onMenuClick = {
|
||||
showOtherUserMenu = true
|
||||
},
|
||||
@@ -580,6 +587,9 @@ fun ProfileV3(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 积分底部弹窗
|
||||
// 全局积分弹窗由 MainActivity 中的 PointsBottomSheetHost 承载
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -598,11 +608,14 @@ fun TopNavigationBar(
|
||||
navController: androidx.navigation.NavController,
|
||||
backgroundAlpha: Float,
|
||||
interactionCount: Int = 0,
|
||||
onPointsClick: (() -> Unit)? = null,
|
||||
onMenuClick: () -> Unit = {},
|
||||
onShareClick: () -> Unit = {}
|
||||
) {
|
||||
val appColors = LocalAppTheme.current
|
||||
val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) }
|
||||
// 仅本人主页显示积分:收集全局积分
|
||||
val pointsBalanceState = if (isSelf) PointService.pointsBalance.collectAsState(initial = null) else null
|
||||
|
||||
// 根据背景透明度和暗色模式决定图标颜色
|
||||
// 暗色模式下:图标始终为白色
|
||||
@@ -704,7 +717,10 @@ fun TopNavigationBar(
|
||||
color = cardBorderColor, // 根据背景透明度改变边框颜色
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
)
|
||||
.padding(horizontal = 8.dp),
|
||||
.padding(horizontal = 8.dp)
|
||||
.let {
|
||||
if (isSelf) it.noRippleClickable { onPointsClick?.invoke() } else it
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
@@ -716,7 +732,11 @@ fun TopNavigationBar(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = numberFormat.format(interactionCount),
|
||||
text = if (isSelf) {
|
||||
pointsBalanceState?.value?.balance?.let { numberFormat.format(it) } ?: "--"
|
||||
} else {
|
||||
numberFormat.format(interactionCount)
|
||||
},
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W500,
|
||||
color = if (AppState.darkMode) Color.White else Color.Black, // 暗色模式下为白色,亮色模式下为黑色
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.search
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -10,6 +11,8 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -32,14 +35,13 @@ import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Tab
|
||||
import androidx.compose.material.TabRow
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -85,10 +87,8 @@ fun SearchScreen() {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val context = LocalContext.current
|
||||
val model = SearchViewModel
|
||||
val categories = listOf(context.getString(R.string.moment), context.getString(R.string.users))
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val pagerState = rememberPagerState(pageCount = { categories.size })
|
||||
val selectedTabIndex = remember { derivedStateOf { pagerState.currentPage } }
|
||||
val pagerState = rememberPagerState(pageCount = { 3 })
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||
@@ -100,11 +100,19 @@ fun SearchScreen() {
|
||||
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !AppState.darkMode)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
model.ensureInit(context)
|
||||
if (model.requestFocus) {
|
||||
focusRequester.requestFocus()
|
||||
model.requestFocus = false
|
||||
}
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
// 离开页面时重置搜索状态与文本
|
||||
model.searchText = ""
|
||||
model.ResetModel()
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -130,7 +138,7 @@ fun SearchScreen() {
|
||||
.weight(1f),
|
||||
text = model.searchText,
|
||||
onTextChange = {
|
||||
model.searchText = it
|
||||
model.onTextChanged(it)
|
||||
},
|
||||
onSearch = {
|
||||
model.search()
|
||||
@@ -144,75 +152,81 @@ fun SearchScreen() {
|
||||
stringResource(R.string.cancel),
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.noRippleClickable {
|
||||
// 退出时也重置,确保返回后显示历史而不是上次结果
|
||||
model.searchText = ""
|
||||
model.ResetModel()
|
||||
navController.navigateUp()
|
||||
},
|
||||
color = AppColors.text
|
||||
)
|
||||
}
|
||||
|
||||
// 历史搜索(当不展示结果时)
|
||||
if (!model.showResult) {
|
||||
HistorySection(
|
||||
onClear = { model.clearHistory() },
|
||||
onClick = { term ->
|
||||
coroutineScope.launch {
|
||||
keyboardController?.hide()
|
||||
model.onTextChanged(term)
|
||||
pagerState.scrollToPage(0)
|
||||
model.search()
|
||||
}
|
||||
},
|
||||
onRemove = { term -> model.removeHistoryItem(term) }
|
||||
)
|
||||
}
|
||||
|
||||
// 添加user、dynamic和ai标签页
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(start = 16.dp, top = 16.dp),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
Box {
|
||||
TabItem(
|
||||
text = stringResource(R.string.moment),
|
||||
isSelected = pagerState.currentPage == 0,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(0)
|
||||
if (model.showResult) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(start = 16.dp, top = 16.dp),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
Box {
|
||||
TabItem(
|
||||
text = stringResource(R.string.moment),
|
||||
isSelected = pagerState.currentPage == 0,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
TabSpacer()
|
||||
Box {
|
||||
TabItem(
|
||||
text = stringResource(R.string.users),
|
||||
isSelected = pagerState.currentPage == 1,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(1)
|
||||
)
|
||||
}
|
||||
TabSpacer()
|
||||
Box {
|
||||
TabItem(
|
||||
text = stringResource(R.string.users),
|
||||
isSelected = pagerState.currentPage == 1,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
TabSpacer()
|
||||
Box {
|
||||
TabItem(
|
||||
text = stringResource(R.string.chat_ai),
|
||||
isSelected = pagerState.currentPage == 2,
|
||||
onClick = {
|
||||
// TODO: 实现点击逻辑
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
TabSpacer()
|
||||
Box {
|
||||
TabItem(
|
||||
text = stringResource(R.string.chat_ai),
|
||||
isSelected = pagerState.currentPage == 2,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(2)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (model.showResult) {
|
||||
TabRow(
|
||||
selectedTabIndex = selectedTabIndex.value,
|
||||
backgroundColor = AppColors.background,
|
||||
contentColor = AppColors.text,
|
||||
) {
|
||||
categories.forEachIndexed { index, category ->
|
||||
Tab(
|
||||
selected = selectedTabIndex.value == index,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(index)
|
||||
}
|
||||
},
|
||||
text = { Text(category, color = AppColors.text) }
|
||||
)
|
||||
}
|
||||
}
|
||||
SearchPager(
|
||||
pagerState = pagerState
|
||||
)
|
||||
@@ -220,6 +234,67 @@ fun SearchScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun HistorySection(
|
||||
onClear: () -> Unit,
|
||||
onClick: (String) -> Unit,
|
||||
onRemove: (String) -> Unit
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val model = SearchViewModel
|
||||
val items = model.historyFlow.collectAsState(initial = emptyList()).value
|
||||
if (items.isEmpty()) return
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "历史搜索",
|
||||
color = AppColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = "清空",
|
||||
color = AppColors.secondaryText,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.noRippleClickable { onClear() }
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items.forEach { term ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(AppColors.inputBackground, RoundedCornerShape(16.dp))
|
||||
.combinedClickable(
|
||||
onClick = { onClick(term) },
|
||||
onLongClick = { onRemove(term) }
|
||||
)
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = term,
|
||||
color = AppColors.text,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchInput(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -300,6 +375,7 @@ fun SearchPager(
|
||||
when (page) {
|
||||
0 -> MomentResultTab()
|
||||
1 -> UserResultTab()
|
||||
2 -> AiResultTab()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -566,6 +642,118 @@ fun UserItem(
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun AiResultTab() {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val context = LocalContext.current
|
||||
val model = SearchViewModel
|
||||
val agents = model.agentsFlow.collectAsLazyPagingItems()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(AppColors.background)
|
||||
) {
|
||||
if (agents.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(agents.itemCount) { idx ->
|
||||
val agent = agents[idx] ?: return@items
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
context,
|
||||
imageUrl = agent.avatar,
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(RoundedCornerShape(24.dp)),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = agent.title,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = AppColors.text
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = agent.desc,
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.secondaryText,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun ReloadButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.search
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -18,6 +19,11 @@ import com.aiosman.ravenow.entity.MomentEntity
|
||||
import com.aiosman.ravenow.entity.MomentPagingSource
|
||||
import com.aiosman.ravenow.entity.MomentRemoteDataSource
|
||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
||||
import com.aiosman.ravenow.entity.AgentEntity
|
||||
import com.aiosman.ravenow.entity.AgentRemoteDataSource
|
||||
import com.aiosman.ravenow.entity.AgentSearchPagingSource
|
||||
import com.aiosman.ravenow.entity.AgentServiceImpl
|
||||
import com.aiosman.ravenow.utils.SearchHistoryStore
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
@@ -32,12 +38,37 @@ object SearchViewModel : ViewModel() {
|
||||
private val userService = UserServiceImpl()
|
||||
private val _usersFlow = MutableStateFlow<PagingData<AccountProfileEntity>>(PagingData.empty())
|
||||
val usersFlow = _usersFlow.asStateFlow()
|
||||
private val _agentsFlow = MutableStateFlow<PagingData<AgentEntity>>(PagingData.empty())
|
||||
val agentsFlow = _agentsFlow.asStateFlow()
|
||||
private lateinit var historyStore: SearchHistoryStore
|
||||
private val _historyFlow = MutableStateFlow<List<String>>(emptyList())
|
||||
val historyFlow = _historyFlow.asStateFlow()
|
||||
var showResult by mutableStateOf(false)
|
||||
var requestFocus by mutableStateOf(false)
|
||||
|
||||
fun ensureInit(context: Context) {
|
||||
if (!::historyStore.isInitialized) {
|
||||
historyStore = SearchHistoryStore(context.applicationContext)
|
||||
_historyFlow.value = historyStore.getHistory()
|
||||
}
|
||||
}
|
||||
|
||||
fun onTextChanged(newText: String) {
|
||||
searchText = newText
|
||||
if (newText.isBlank()) {
|
||||
showResult = false
|
||||
}
|
||||
}
|
||||
|
||||
fun search() {
|
||||
if (searchText.isEmpty()) {
|
||||
return
|
||||
}
|
||||
// 记录历史
|
||||
if (::historyStore.isInitialized) {
|
||||
historyStore.add(searchText)
|
||||
_historyFlow.value = historyStore.getHistory()
|
||||
}
|
||||
viewModelScope.launch {
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||
@@ -64,9 +95,34 @@ object SearchViewModel : ViewModel() {
|
||||
_usersFlow.value = it
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
AgentSearchPagingSource(
|
||||
AgentRemoteDataSource(AgentServiceImpl()),
|
||||
keyword = searchText
|
||||
)
|
||||
}
|
||||
).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_agentsFlow.value = it
|
||||
}
|
||||
}
|
||||
showResult = true
|
||||
}
|
||||
|
||||
fun removeHistoryItem(term: String) {
|
||||
if (!::historyStore.isInitialized) return
|
||||
historyStore.remove(term)
|
||||
_historyFlow.value = historyStore.getHistory()
|
||||
}
|
||||
|
||||
fun clearHistory() {
|
||||
if (!::historyStore.isInitialized) return
|
||||
historyStore.clear()
|
||||
_historyFlow.value = emptyList()
|
||||
}
|
||||
|
||||
suspend fun followUser(id:Int){
|
||||
userService.followUser(id.toString())
|
||||
val currentPagingData = _usersFlow.value
|
||||
@@ -96,6 +152,7 @@ object SearchViewModel : ViewModel() {
|
||||
fun ResetModel(){
|
||||
_momentsFlow.value = PagingData.empty()
|
||||
_usersFlow.value = PagingData.empty()
|
||||
_agentsFlow.value = PagingData.empty()
|
||||
showResult = false
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
package com.aiosman.ravenow.ui.points
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
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.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.ui.composables.TabItem
|
||||
import com.aiosman.ravenow.ui.composables.TabSpacer
|
||||
import com.aiosman.ravenow.data.PointService
|
||||
import com.aiosman.ravenow.data.api.PointsChangeLog
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PointsBottomSheet(
|
||||
onClose: () -> Unit,
|
||||
onRecharge: () -> Unit
|
||||
){
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val AppColors = LocalAppTheme.current
|
||||
val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) }
|
||||
val balanceState = PointsViewModel.balance.collectAsState(initial = null)
|
||||
|
||||
var tab by remember { mutableStateOf(0) } // 0: history, 1: how to earn
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
PointsViewModel.initLoad()
|
||||
}
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onClose,
|
||||
sheetState = sheetState,
|
||||
containerColor = AppColors.background
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.98f)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
// 头部
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(text = "My Pai Coin", color = AppColors.text, fontSize = 20.sp, fontWeight = FontWeight.Bold)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(Color(0xFFF1E9FF))
|
||||
.clickable { onRecharge() }
|
||||
.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||
) {
|
||||
Text(text = "Recharge", color = Color(0xFF6B46C1), fontSize = 14.sp, fontWeight = FontWeight.W600,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.padding(0.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
// 余额卡片
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(AppColors.nonActive)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text("Current Balance", color = AppColors.secondaryText, fontSize = 14.sp)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.paip_coin_img),
|
||||
contentDescription = "coin",
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(
|
||||
text = balanceState.value?.balance?.let { numberFormat.format(it) } ?: "--",
|
||||
color = AppColors.text,
|
||||
fontSize = 36.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.weight(1f)) {
|
||||
Text(text = numberFormat.format(balanceState.value?.totalEarned ?: 0), color = AppColors.text, fontSize = 18.sp, fontWeight = FontWeight.W700)
|
||||
Text(text = "Total Earned", color = AppColors.secondaryText, fontSize = 12.sp)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(22.dp)
|
||||
.background(AppColors.divider)
|
||||
.width(1.dp)
|
||||
)
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.weight(1f)) {
|
||||
Text(text = numberFormat.format(balanceState.value?.totalSpent ?: 0), color = AppColors.text, fontSize = 18.sp, fontWeight = FontWeight.W700)
|
||||
Text(text = "Total Spent", color = AppColors.secondaryText, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
// 分段切换
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TabItem(
|
||||
text = "Transaction History",
|
||||
isSelected = tab == 0,
|
||||
onClick = { tab = 0 },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
TabSpacer()
|
||||
TabItem(
|
||||
text = "How to Earn",
|
||||
isSelected = tab == 1,
|
||||
onClick = { tab = 1 },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
if (tab == 0) {
|
||||
PointsHistoryList(
|
||||
items = PointsViewModel.logs,
|
||||
onLoadMore = { PointsViewModel.loadMore() },
|
||||
hasNext = PointsViewModel.hasNext
|
||||
)
|
||||
} else {
|
||||
HowToEarnList()
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SegmentItem(selected: Boolean, text: String, onClick: () -> Unit) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(if (selected) AppColors.background else Color.Transparent)
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(text = text, color = if (selected) AppColors.text else AppColors.secondaryText, fontSize = 14.sp, fontWeight = FontWeight.W600)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PointsHistoryList(
|
||||
items: List<PointsChangeLog>,
|
||||
onLoadMore: () -> Unit,
|
||||
hasNext: Boolean
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) }
|
||||
LazyColumn {
|
||||
items(items) { item ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.paip_coin_img),
|
||||
contentDescription = "reason",
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
)
|
||||
Spacer(Modifier.size(12.dp))
|
||||
Column {
|
||||
Text(text = PointService.getReasonDescription(item.reason ?: "" ), color = AppColors.text, fontSize = 16.sp, fontWeight = FontWeight.W600)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(text = item.createdAt ?: "", color = AppColors.secondaryText, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
val amount = item.amount ?: 0
|
||||
val isPositive = amount >= 0
|
||||
val amountColor = if (isPositive) Color(0xFF00C853) else Color(0xFFFF1744)
|
||||
Text(
|
||||
text = (if (isPositive) "+" else "") + numberFormat.format(amount),
|
||||
color = amountColor,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
if (hasNext) {
|
||||
item {
|
||||
Button(onClick = onLoadMore, modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)) {
|
||||
Text("Load More")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HowToEarnList() {
|
||||
val AppColors = LocalAppTheme.current
|
||||
@Composable
|
||||
fun RowItem(title: String, desc: String, amount: String) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.paip_coin_img),
|
||||
contentDescription = "earn",
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
)
|
||||
Spacer(Modifier.size(12.dp))
|
||||
Column {
|
||||
Text(text = title, color = AppColors.text, fontSize = 16.sp, fontWeight = FontWeight.W600)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(text = desc, color = AppColors.secondaryText, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
Text(text = amount, color = Color(0xFF00C853), fontSize = 16.sp, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
RowItem("New User Reward", "Register and get 500 Pai Coin", "+500")
|
||||
RowItem("Daily Check-in", "Check in daily to earn Pai Coin", "+10-50")
|
||||
RowItem("Invite Friends", "Earn Pai Coin for each friend invited", "+100")
|
||||
RowItem("Complete Tasks", "Complete tasks to earn rewards", "+20-200")
|
||||
RowItem("Recharge Pai Coin", "Multiple packages available, recharge now", ">")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.aiosman.ravenow.ui.points
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
|
||||
@Composable
|
||||
fun PointsBottomSheetHost() {
|
||||
val show = PointsSheetManager.visible.collectAsState(false).value
|
||||
if (show) {
|
||||
PointsBottomSheet(
|
||||
onClose = { PointsSheetManager.close() },
|
||||
onRecharge = { PointsSheetManager.onRecharge?.invoke() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.aiosman.ravenow.ui.points
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
object PointsSheetManager {
|
||||
private val _visible = MutableStateFlow(false)
|
||||
val visible: StateFlow<Boolean> = _visible.asStateFlow()
|
||||
var onRecharge: (() -> Unit)? = null
|
||||
|
||||
fun open(onRecharge: (() -> Unit)? = null) {
|
||||
this.onRecharge = onRecharge
|
||||
_visible.value = true
|
||||
PointsViewModel.initLoad()
|
||||
}
|
||||
|
||||
fun close() {
|
||||
_visible.value = false
|
||||
onRecharge = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.aiosman.ravenow.ui.points
|
||||
|
||||
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.PointService
|
||||
import com.aiosman.ravenow.data.api.PointsChangeLog
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object PointsViewModel : ViewModel() {
|
||||
val balance = PointService.pointsBalance
|
||||
var logs by mutableStateOf<List<PointsChangeLog>>(emptyList())
|
||||
var page by mutableStateOf(1)
|
||||
var hasNext by mutableStateOf(true)
|
||||
var loading by mutableStateOf(false)
|
||||
|
||||
fun initLoad() {
|
||||
if (loading) return
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
loading = true
|
||||
if (!AppStore.isGuest) {
|
||||
PointService.refreshMyPointsBalance()
|
||||
}
|
||||
val r = PointService.getMyPointsChangeLogs(page = 1, pageSize = 20)
|
||||
logs = r.list
|
||||
hasNext = (r.page * r.pageSize) < r.total
|
||||
page = r.page + 1
|
||||
} catch (e: Exception) {
|
||||
Log.e("PointsViewModel", "initLoad error", e)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMore() {
|
||||
if (!hasNext || loading) return
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
loading = true
|
||||
val r = PointService.getMyPointsChangeLogs(page = page, pageSize = 20)
|
||||
logs = logs + r.list
|
||||
hasNext = (r.page * r.pageSize) < r.total
|
||||
page = r.page + 1
|
||||
} catch (e: Exception) {
|
||||
Log.e("PointsViewModel", "loadMore error", e)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
171
app/src/main/java/com/aiosman/ravenow/ui/scan/ScanQrScreen.kt
Normal file
171
app/src/main/java/com/aiosman/ravenow/ui/scan/ScanQrScreen.kt
Normal file
@@ -0,0 +1,171 @@
|
||||
package com.aiosman.ravenow.ui.scan
|
||||
|
||||
import android.Manifest
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@Composable
|
||||
fun ScanQrScreen() {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val navController = LocalNavController.current
|
||||
|
||||
var cameraGranted by remember { mutableStateOf<Boolean?>(null) }
|
||||
var hasResult by remember { mutableStateOf(false) }
|
||||
|
||||
val requestPermissionLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestPermission(),
|
||||
onResult = { granted ->
|
||||
cameraGranted = granted
|
||||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||
}
|
||||
|
||||
val scanner = remember {
|
||||
val options = BarcodeScannerOptions.Builder()
|
||||
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
|
||||
.build()
|
||||
BarcodeScanning.getClient(options)
|
||||
}
|
||||
|
||||
when (cameraGranted) {
|
||||
null -> {
|
||||
// 等待权限结果
|
||||
Box(modifier = Modifier.fillMaxSize().background(Color.Black))
|
||||
}
|
||||
false -> {
|
||||
// 权限被拒绝
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().background(Color.Black),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(text = "需要相机权限以扫码", color = Color.White, modifier = Modifier.padding(16.dp))
|
||||
Button(onClick = { navController.popBackStack() }) {
|
||||
Text(text = "返回")
|
||||
}
|
||||
}
|
||||
}
|
||||
true -> {
|
||||
Box(modifier = Modifier.fillMaxSize().background(Color.Black)) {
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = { ctx ->
|
||||
PreviewView(ctx).apply {
|
||||
this.scaleType = PreviewView.ScaleType.FILL_CENTER
|
||||
}
|
||||
},
|
||||
update = { previewView ->
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||
cameraProviderFuture.addListener({
|
||||
val cameraProvider = cameraProviderFuture.get()
|
||||
|
||||
val preview = Preview.Builder().build().also { p ->
|
||||
p.setSurfaceProvider(previewView.surfaceProvider)
|
||||
}
|
||||
|
||||
val analysis = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
.also { imageAnalysis ->
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
imageAnalysis.setAnalyzer(executor) { imageProxy ->
|
||||
val mediaImage = imageProxy.image
|
||||
if (mediaImage == null) {
|
||||
imageProxy.close()
|
||||
return@setAnalyzer
|
||||
}
|
||||
val image = InputImage.fromMediaImage(
|
||||
mediaImage,
|
||||
imageProxy.imageInfo.rotationDegrees
|
||||
)
|
||||
scanner.process(image)
|
||||
.addOnSuccessListener { barcodes ->
|
||||
if (!hasResult && !barcodes.isNullOrEmpty()) {
|
||||
val first = barcodes.firstOrNull()
|
||||
val rawValue = first?.rawValue ?: first?.displayValue
|
||||
if (!rawValue.isNullOrBlank()) {
|
||||
hasResult = true
|
||||
navController.previousBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.set("scan_result", rawValue)
|
||||
Toast.makeText(context, rawValue, Toast.LENGTH_SHORT).show()
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
.addOnFailureListener {
|
||||
// 忽略单帧失败
|
||||
}
|
||||
.addOnCompleteListener {
|
||||
imageProxy.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
cameraProvider.unbindAll()
|
||||
cameraProvider.bindToLifecycle(
|
||||
lifecycleOwner,
|
||||
CameraSelector.DEFAULT_BACK_CAMERA,
|
||||
preview,
|
||||
analysis
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
// 绑定失败,忽略
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(context))
|
||||
}
|
||||
)
|
||||
|
||||
// 简单文案提示
|
||||
Text(
|
||||
text = "对准二维码进行扫描",
|
||||
color = Color.White,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.aiosman.ravenow.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
|
||||
/**
|
||||
* 搜索历史存储(SharedPreferences + JSON)
|
||||
* - 最多保留 maxSize 条
|
||||
* - 新记录放首位,去重(忽略前后空格)
|
||||
*/
|
||||
class SearchHistoryStore(context: Context) {
|
||||
private val prefs: SharedPreferences =
|
||||
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||
private val gson = Gson()
|
||||
|
||||
fun getHistory(): List<String> {
|
||||
val json = prefs.getString(KEY_HISTORY, "[]") ?: "[]"
|
||||
return runCatching {
|
||||
val type = object : TypeToken<List<String>>() {}.type
|
||||
gson.fromJson<List<String>>(json, type) ?: emptyList()
|
||||
}.getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
fun add(term: String) {
|
||||
val normalized = term.trim()
|
||||
if (normalized.isEmpty()) return
|
||||
val current = getHistory().toMutableList()
|
||||
current.removeAll { it.equals(normalized, ignoreCase = true) }
|
||||
current.add(0, normalized)
|
||||
while (current.size > MAX_SIZE) current.removeLast()
|
||||
save(current)
|
||||
}
|
||||
|
||||
fun remove(term: String) {
|
||||
val normalized = term.trim()
|
||||
val current = getHistory().toMutableList()
|
||||
current.removeAll { it.equals(normalized, ignoreCase = true) }
|
||||
save(current)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
save(emptyList())
|
||||
}
|
||||
|
||||
private fun save(list: List<String>) {
|
||||
prefs.edit().putString(KEY_HISTORY, gson.toJson(list)).apply()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_NAME = "search_history_pref"
|
||||
private const val KEY_HISTORY = "search_history_v1"
|
||||
private const val MAX_SIZE = 10
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ lifecycleProcess = "2.8.4"
|
||||
playServicesAuth = "21.4.0"
|
||||
rendering = "1.17.1"
|
||||
zoomable = "1.6.1"
|
||||
camerax = "1.3.4"
|
||||
mlkitBarcode = "17.3.0"
|
||||
|
||||
[libraries]
|
||||
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" }
|
||||
@@ -56,6 +58,9 @@ androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "medi
|
||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
|
||||
androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pagingRuntime" }
|
||||
androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "pagingRuntime" }
|
||||
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" }
|
||||
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" }
|
||||
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "camerax" }
|
||||
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
|
||||
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
||||
compose-image-blurhash = { module = "com.github.orlando-dev-code:compose-image-blurhash", version.ref = "composeImageBlurhash" }
|
||||
@@ -99,6 +104,7 @@ rendering = { group = "com.google.ar.sceneform", name = "rendering", version.ref
|
||||
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "converterGson" }
|
||||
zoomable = { module = "net.engawapg.lib:zoomable", version.ref = "zoomable" }
|
||||
lottie = { module="com.airbnb.android:lottie-compose", version="6.6.10"}
|
||||
mlkit-barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "mlkitBarcode" }
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
|
||||
Reference in New Issue
Block a user