@@ -127,5 +127,11 @@ dependencies {
|
|||||||
implementation (libs.eventbus)
|
implementation (libs.eventbus)
|
||||||
implementation(libs.lottie)
|
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.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
<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
|
<application
|
||||||
android:name=".RaveNowApplication"
|
android:name=".RaveNowApplication"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.aiosman.ravenow.data.AccountService
|
|||||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
import com.aiosman.ravenow.data.DictService
|
import com.aiosman.ravenow.data.DictService
|
||||||
import com.aiosman.ravenow.data.DictServiceImpl
|
import com.aiosman.ravenow.data.DictServiceImpl
|
||||||
|
import com.aiosman.ravenow.data.PointService
|
||||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
import com.aiosman.ravenow.ui.favourite.FavouriteListViewModel
|
import com.aiosman.ravenow.ui.favourite.FavouriteListViewModel
|
||||||
import com.aiosman.ravenow.ui.favourite.FavouriteNoticeViewModel
|
import com.aiosman.ravenow.ui.favourite.FavouriteNoticeViewModel
|
||||||
@@ -81,6 +82,14 @@ object AppState {
|
|||||||
// 注册 JPush
|
// 注册 JPush
|
||||||
Messaging.registerDevice(scope, context)
|
Messaging.registerDevice(scope, context)
|
||||||
initChat(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()
|
AgentViewModel.ResetModel()
|
||||||
MineAgentViewModel.ResetModel()
|
MineAgentViewModel.ResetModel()
|
||||||
UserId = null
|
UserId = null
|
||||||
|
// 清空积分全局状态,避免用户切换串号
|
||||||
|
PointService.clear()
|
||||||
|
|
||||||
// 清除游客状态
|
// 清除游客状态
|
||||||
AppStore.isGuest = false
|
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.dialogs.CheckUpdateDialog
|
||||||
import com.aiosman.ravenow.ui.navigateToPost
|
import com.aiosman.ravenow.ui.navigateToPost
|
||||||
import com.aiosman.ravenow.ui.post.NewPostViewModel
|
import com.aiosman.ravenow.ui.post.NewPostViewModel
|
||||||
|
import com.aiosman.ravenow.ui.points.PointsBottomSheetHost
|
||||||
import com.google.firebase.Firebase
|
import com.google.firebase.Firebase
|
||||||
import com.google.firebase.analytics.FirebaseAnalytics
|
import com.google.firebase.analytics.FirebaseAnalytics
|
||||||
import com.google.firebase.analytics.analytics
|
import com.google.firebase.analytics.analytics
|
||||||
@@ -141,6 +142,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
LocalAppTheme provides AppState.appTheme
|
LocalAppTheme provides AppState.appTheme
|
||||||
) {
|
) {
|
||||||
CheckUpdateDialog()
|
CheckUpdateDialog()
|
||||||
|
// 全局挂载积分底部弹窗 Host
|
||||||
|
PointsBottomSheetHost()
|
||||||
Navigation(startDestination) { navController ->
|
Navigation(startDestination) { navController ->
|
||||||
|
|
||||||
// 处理带有 postId 的通知点击
|
// 处理带有 postId 的通知点击
|
||||||
|
|||||||
@@ -108,6 +108,15 @@ interface AgentService {
|
|||||||
authorId: Int? = null
|
authorId: Int? = null
|
||||||
): ListContainer<AgentEntity>?
|
): 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.RoomRuleQuota
|
||||||
import com.aiosman.ravenow.data.api.RoomRule
|
import com.aiosman.ravenow.data.api.RoomRule
|
||||||
import com.aiosman.ravenow.data.api.RoomRuleCreator
|
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.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.RoomEntity
|
||||||
import com.aiosman.ravenow.entity.RoomRuleEntity
|
import com.aiosman.ravenow.entity.RoomRuleEntity
|
||||||
import com.aiosman.ravenow.entity.RoomRuleCreatorEntity
|
import com.aiosman.ravenow.entity.RoomRuleCreatorEntity
|
||||||
@@ -173,6 +185,68 @@ interface RoomService {
|
|||||||
roomId: Int? = null,
|
roomId: Int? = null,
|
||||||
trtcId: String? = null
|
trtcId: String? = null
|
||||||
): RoomRuleQuotaEntity
|
): 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()
|
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,
|
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 错误响应(用于加入房间等接口的错误处理)
|
// API 错误响应(用于加入房间等接口的错误处理)
|
||||||
data class ApiErrorResponse(
|
data class ApiErrorResponse(
|
||||||
@SerializedName("err")
|
@SerializedName("err")
|
||||||
@@ -762,6 +1016,71 @@ data class InsufficientBalanceError(
|
|||||||
val traceId: String?
|
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 {
|
interface RaveNowAPI {
|
||||||
@GET("membership/config")
|
@GET("membership/config")
|
||||||
@retrofit2.http.Headers("X-Requires-Auth: true")
|
@retrofit2.http.Headers("X-Requires-Auth: true")
|
||||||
@@ -1605,5 +1924,209 @@ interface RaveNowAPI {
|
|||||||
@Query("promptReplaceTrans") promptReplaceTrans: Boolean? = null
|
@Query("promptReplaceTrans") promptReplaceTrans: Boolean? = null
|
||||||
): Response<RecommendationsResponse>
|
): 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(
|
class AgentRemoteDataSource(
|
||||||
private val agentService: AgentService,
|
private val agentService: AgentService,
|
||||||
@@ -103,6 +132,16 @@ class AgentRemoteDataSource(
|
|||||||
authorId = authorId
|
authorId = authorId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun searchAgentByTitle(
|
||||||
|
pageNumber: Int,
|
||||||
|
title: String
|
||||||
|
): ListContainer<AgentEntity>? {
|
||||||
|
return agentService.searchAgentByTitle(
|
||||||
|
pageNumber = pageNumber,
|
||||||
|
title = title
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AgentServiceImpl() : AgentService {
|
class AgentServiceImpl() : AgentService {
|
||||||
@@ -118,6 +157,17 @@ class AgentServiceImpl() : AgentService {
|
|||||||
authorId = authorId
|
authorId = authorId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun searchAgentByTitle(
|
||||||
|
pageNumber: Int,
|
||||||
|
pageSize: Int,
|
||||||
|
title: String
|
||||||
|
): ListContainer<AgentEntity>? {
|
||||||
|
return agentBackend.searchAgentByTitle(
|
||||||
|
pageNumber = pageNumber,
|
||||||
|
title = title
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AgentBackend {
|
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(
|
data class AgentEntity(
|
||||||
|
|||||||
@@ -86,6 +86,128 @@ data class RoomRuleQuotaEntity(
|
|||||||
val usagePercent: Double
|
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>() {
|
class RoomLoader : DataLoader<AgentEntity,AgentLoaderExtraArgs>() {
|
||||||
override suspend fun fetchData(
|
override suspend fun fetchData(
|
||||||
page: Int,
|
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.profile.AccountProfileV2
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.vip.VipSelPage
|
import com.aiosman.ravenow.ui.index.tabs.profile.vip.VipSelPage
|
||||||
import com.aiosman.ravenow.ui.notification.NotificationScreen
|
import com.aiosman.ravenow.ui.notification.NotificationScreen
|
||||||
|
import com.aiosman.ravenow.ui.scan.ScanQrScreen
|
||||||
|
|
||||||
sealed class NavigationRoute(
|
sealed class NavigationRoute(
|
||||||
val route: String,
|
val route: String,
|
||||||
@@ -130,6 +131,7 @@ sealed class NavigationRoute(
|
|||||||
data object NotificationScreen : NavigationRoute("NotificationScreen")
|
data object NotificationScreen : NavigationRoute("NotificationScreen")
|
||||||
data object MbtiSelect : NavigationRoute("MbtiSelect")
|
data object MbtiSelect : NavigationRoute("MbtiSelect")
|
||||||
data object ZodiacSelect : NavigationRoute("ZodiacSelect")
|
data object ZodiacSelect : NavigationRoute("ZodiacSelect")
|
||||||
|
data object ScanQr : NavigationRoute("ScanQr")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -453,6 +455,9 @@ fun NavigationController(
|
|||||||
SearchScreen()
|
SearchScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
composable(route = NavigationRoute.ScanQr.route) {
|
||||||
|
ScanQrScreen()
|
||||||
|
}
|
||||||
composable(
|
composable(
|
||||||
route = NavigationRoute.FollowerList.route,
|
route = NavigationRoute.FollowerList.route,
|
||||||
arguments = listOf(navArgument("id") { type = NavType.IntType })
|
arguments = listOf(navArgument("id") { type = NavType.IntType })
|
||||||
|
|||||||
@@ -586,7 +586,11 @@ fun SideMenuContent(
|
|||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
.offset(x = (-112).dp, y = 88.dp)
|
.offset(x = (-112).dp, y = 88.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
// TODO: 实现扫一扫功能
|
// 扫一扫功能:跳转到扫码页面
|
||||||
|
coroutineScope.launch {
|
||||||
|
onClose()
|
||||||
|
navController.navigate(NavigationRoute.ScanQr.route)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
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.foundation.lazy.grid.items as gridItems
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
import androidx.compose.ui.draw.alpha
|
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 {
|
fun LazyListState.isScrolledToEnd(buffer: Int = 3): Boolean {
|
||||||
@@ -273,48 +277,50 @@ fun Agent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 热门聊天室
|
if (viewModel.chatRooms.isNotEmpty()) {
|
||||||
stickyHeader(key = "hot_rooms_header") {
|
// 热门聊天室
|
||||||
Row(
|
stickyHeader(key = "hot_rooms_header") {
|
||||||
modifier = Modifier
|
Row(
|
||||||
.fillMaxWidth()
|
modifier = Modifier
|
||||||
.background(AppColors.background)
|
.fillMaxWidth()
|
||||||
.padding(top = 8.dp, bottom = 12.dp),
|
.background(AppColors.background)
|
||||||
horizontalArrangement = Arrangement.Start,
|
.padding(top = 8.dp, bottom = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
horizontalArrangement = Arrangement.Start,
|
||||||
) {
|
verticalAlignment = Alignment.CenterVertically
|
||||||
Image(
|
) {
|
||||||
painter = painterResource(R.mipmap.rider_pro_hot_room),
|
Image(
|
||||||
contentDescription = "chat room",
|
painter = painterResource(R.mipmap.rider_pro_hot_room),
|
||||||
modifier = Modifier.size(28.dp)
|
contentDescription = "chat room",
|
||||||
)
|
modifier = Modifier.size(28.dp)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
)
|
||||||
androidx.compose.material3.Text(
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
text = stringResource(R.string.hot_rooms),
|
androidx.compose.material3.Text(
|
||||||
fontSize = 16.sp,
|
text = stringResource(R.string.hot_rooms),
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
|
fontSize = 16.sp,
|
||||||
color = AppColors.text
|
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 (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
|
@Composable
|
||||||
fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel) {
|
fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
if (agentItems.isEmpty()) return
|
||||||
|
|
||||||
// 每页显示5个agent
|
val pagerState = rememberPagerState(pageCount = { agentItems.size })
|
||||||
val itemsPerPage = 5
|
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
|
||||||
val totalPages = (agentItems.size + itemsPerPage - 1) / itemsPerPage
|
val cardAspect = 1133.5f / 846.4f
|
||||||
|
// 外层 LazyColumn 左右各 8dp + Pager contentPadding 左右各 20dp
|
||||||
|
val horizontalPaddings = 56.dp
|
||||||
|
val pagerHeight = (screenWidth - horizontalPaddings) * cardAspect
|
||||||
|
|
||||||
if (totalPages > 0) {
|
Column {
|
||||||
val pagerState = rememberPagerState(pageCount = { totalPages })
|
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 {
|
AgentLargeCard(
|
||||||
// Agent内容
|
agentItem = agentItems[page],
|
||||||
Box(
|
viewModel = viewModel,
|
||||||
modifier = Modifier
|
navController = LocalNavController.current,
|
||||||
.height(310.dp)
|
modifier = Modifier
|
||||||
) {
|
.graphicsLayer {
|
||||||
HorizontalPager(
|
scaleX = scale
|
||||||
state = pagerState,
|
scaleY = scale
|
||||||
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)
|
|
||||||
|
|
||||||
// 根据偏移量计算缩放比例
|
@SuppressLint("SuspiciousIndentation")
|
||||||
val scale = 1f - (0.1f * kotlin.math.abs(pageOffset))
|
@Composable
|
||||||
|
fun AgentLargeCard(
|
||||||
|
agentItem: AgentItem,
|
||||||
|
viewModel: AgentViewModel,
|
||||||
|
navController: NavHostController,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
var lastClickTime by remember { mutableStateOf(0L) }
|
||||||
|
|
||||||
AgentPage(
|
Box(
|
||||||
viewModel = viewModel,
|
modifier = modifier
|
||||||
agentItems = agentItems.drop(page * itemsPerPage).take(itemsPerPage),
|
.fillMaxWidth()
|
||||||
page = page,
|
.aspectRatio(846.4f / 1133.5f)
|
||||||
modifier = Modifier
|
.clip(RoundedCornerShape(24.dp))
|
||||||
.height(310.dp)
|
.noRippleClickable {
|
||||||
.graphicsLayer {
|
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
||||||
scaleX = scale
|
viewModel.goToProfile(agentItem.openId, navController)
|
||||||
scaleY = scale
|
}) {
|
||||||
},
|
lastClickTime = System.currentTimeMillis()
|
||||||
navController = LocalNavController.current,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
) {
|
||||||
|
// 背景大图
|
||||||
|
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
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(30.dp)
|
.padding(bottom = 56.dp) // 为底部聊天按钮预留空间
|
||||||
.padding(top = 12.dp),
|
|
||||||
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.Center
|
|
||||||
) {
|
) {
|
||||||
repeat(totalPages) { index ->
|
androidx.compose.material3.Text(
|
||||||
Box(
|
text = agentItem.title,
|
||||||
modifier = Modifier
|
color = Color.White,
|
||||||
.padding(horizontal = 4.dp)
|
fontSize = 20.sp,
|
||||||
.size(3.dp)
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W700,
|
||||||
.background(
|
maxLines = 1,
|
||||||
color = if (pagerState.currentPage == index) AppColors.text else AppColors.secondaryText.copy(
|
overflow = TextOverflow.Ellipsis
|
||||||
alpha = 0.3f
|
)
|
||||||
),
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
shape = androidx.compose.foundation.shape.CircleShape
|
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.MomentFavouriteChangeEvent
|
||||||
import com.aiosman.ravenow.event.MomentLikeChangeEvent
|
import com.aiosman.ravenow.event.MomentLikeChangeEvent
|
||||||
import com.aiosman.ravenow.event.MomentRemoveEvent
|
import com.aiosman.ravenow.event.MomentRemoveEvent
|
||||||
|
import com.aiosman.ravenow.data.PointService
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.greenrobot.eventbus.Subscribe
|
import org.greenrobot.eventbus.Subscribe
|
||||||
@@ -76,6 +77,14 @@ object MyProfileViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
firstLoad = false
|
firstLoad = false
|
||||||
loadUserProfile()
|
loadUserProfile()
|
||||||
|
// 刷新积分(仅非游客)
|
||||||
|
if (!AppStore.isGuest) {
|
||||||
|
try {
|
||||||
|
PointService.refreshMyPointsBalance()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("MyProfileViewModel", "refresh points error: ", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
refreshing = false
|
refreshing = false
|
||||||
|
|
||||||
// 游客模式下不加载个人动态和智能体
|
// 游客模式下不加载个人动态和智能体
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import androidx.compose.ui.layout.onGloballyPositioned
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -74,6 +75,7 @@ import com.aiosman.ravenow.LocalAppTheme
|
|||||||
import com.aiosman.ravenow.LocalNavController
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.ravenow.MainActivity
|
import com.aiosman.ravenow.MainActivity
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.data.PointService
|
||||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
import com.aiosman.ravenow.entity.AgentEntity
|
import com.aiosman.ravenow.entity.AgentEntity
|
||||||
import com.aiosman.ravenow.entity.MomentEntity
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
@@ -108,6 +110,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import com.aiosman.ravenow.ui.points.PointsBottomSheet
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -165,6 +168,7 @@ fun ProfileV3(
|
|||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val gridState = rememberLazyGridState()
|
val gridState = rememberLazyGridState()
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
val pointsBalanceState = PointService.pointsBalance.collectAsState(initial = null)
|
||||||
|
|
||||||
// 计算导航栏背景透明度,根据滚动位置从0到1
|
// 计算导航栏背景透明度,根据滚动位置从0到1
|
||||||
val toolbarBackgroundAlpha by remember {
|
val toolbarBackgroundAlpha by remember {
|
||||||
@@ -506,6 +510,9 @@ fun ProfileV3(
|
|||||||
navController = navController,
|
navController = navController,
|
||||||
backgroundAlpha = toolbarBackgroundAlpha,
|
backgroundAlpha = toolbarBackgroundAlpha,
|
||||||
interactionCount = moments.sumOf { it.likeCount }, // 计算总点赞数作为互动数据
|
interactionCount = moments.sumOf { it.likeCount }, // 计算总点赞数作为互动数据
|
||||||
|
onPointsClick = {
|
||||||
|
if (isSelf) com.aiosman.ravenow.ui.points.PointsSheetManager.open()
|
||||||
|
},
|
||||||
onMenuClick = {
|
onMenuClick = {
|
||||||
showOtherUserMenu = true
|
showOtherUserMenu = true
|
||||||
},
|
},
|
||||||
@@ -580,6 +587,9 @@ fun ProfileV3(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 积分底部弹窗
|
||||||
|
// 全局积分弹窗由 MainActivity 中的 PointsBottomSheetHost 承载
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -598,11 +608,14 @@ fun TopNavigationBar(
|
|||||||
navController: androidx.navigation.NavController,
|
navController: androidx.navigation.NavController,
|
||||||
backgroundAlpha: Float,
|
backgroundAlpha: Float,
|
||||||
interactionCount: Int = 0,
|
interactionCount: Int = 0,
|
||||||
|
onPointsClick: (() -> Unit)? = null,
|
||||||
onMenuClick: () -> Unit = {},
|
onMenuClick: () -> Unit = {},
|
||||||
onShareClick: () -> Unit = {}
|
onShareClick: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val appColors = LocalAppTheme.current
|
val appColors = LocalAppTheme.current
|
||||||
val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) }
|
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, // 根据背景透明度改变边框颜色
|
color = cardBorderColor, // 根据背景透明度改变边框颜色
|
||||||
shape = RoundedCornerShape(16.dp)
|
shape = RoundedCornerShape(16.dp)
|
||||||
)
|
)
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp)
|
||||||
|
.let {
|
||||||
|
if (isSelf) it.noRippleClickable { onPointsClick?.invoke() } else it
|
||||||
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
@@ -716,7 +732,11 @@ fun TopNavigationBar(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = numberFormat.format(interactionCount),
|
text = if (isSelf) {
|
||||||
|
pointsBalanceState?.value?.balance?.let { numberFormat.format(it) } ?: "--"
|
||||||
|
} else {
|
||||||
|
numberFormat.format(interactionCount)
|
||||||
|
},
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.W500,
|
fontWeight = FontWeight.W500,
|
||||||
color = if (AppState.darkMode) Color.White else Color.Black, // 暗色模式下为白色,亮色模式下为黑色
|
color = if (AppState.darkMode) Color.White else Color.Black, // 暗色模式下为白色,亮色模式下为黑色
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.aiosman.ravenow.ui.index.tabs.search
|
package com.aiosman.ravenow.ui.index.tabs.search
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
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.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
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.Button
|
||||||
import androidx.compose.material.ButtonDefaults
|
import androidx.compose.material.ButtonDefaults
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.Tab
|
|
||||||
import androidx.compose.material.TabRow
|
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
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.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -85,10 +87,8 @@ fun SearchScreen() {
|
|||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val model = SearchViewModel
|
val model = SearchViewModel
|
||||||
val categories = listOf(context.getString(R.string.moment), context.getString(R.string.users))
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val pagerState = rememberPagerState(pageCount = { categories.size })
|
val pagerState = rememberPagerState(pageCount = { 3 })
|
||||||
val selectedTabIndex = remember { derivedStateOf { pagerState.currentPage } }
|
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
val systemUiController = rememberSystemUiController()
|
val systemUiController = rememberSystemUiController()
|
||||||
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||||
@@ -100,11 +100,19 @@ fun SearchScreen() {
|
|||||||
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !AppState.darkMode)
|
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !AppState.darkMode)
|
||||||
}
|
}
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
model.ensureInit(context)
|
||||||
if (model.requestFocus) {
|
if (model.requestFocus) {
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
model.requestFocus = false
|
model.requestFocus = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
// 离开页面时重置搜索状态与文本
|
||||||
|
model.searchText = ""
|
||||||
|
model.ResetModel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -130,7 +138,7 @@ fun SearchScreen() {
|
|||||||
.weight(1f),
|
.weight(1f),
|
||||||
text = model.searchText,
|
text = model.searchText,
|
||||||
onTextChange = {
|
onTextChange = {
|
||||||
model.searchText = it
|
model.onTextChanged(it)
|
||||||
},
|
},
|
||||||
onSearch = {
|
onSearch = {
|
||||||
model.search()
|
model.search()
|
||||||
@@ -144,75 +152,81 @@ fun SearchScreen() {
|
|||||||
stringResource(R.string.cancel),
|
stringResource(R.string.cancel),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
modifier = Modifier.noRippleClickable {
|
modifier = Modifier.noRippleClickable {
|
||||||
|
// 退出时也重置,确保返回后显示历史而不是上次结果
|
||||||
|
model.searchText = ""
|
||||||
|
model.ResetModel()
|
||||||
navController.navigateUp()
|
navController.navigateUp()
|
||||||
},
|
},
|
||||||
color = AppColors.text
|
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标签页
|
// 添加user、dynamic和ai标签页
|
||||||
Row(
|
if (model.showResult) {
|
||||||
modifier = Modifier
|
Row(
|
||||||
.fillMaxWidth()
|
modifier = Modifier
|
||||||
.wrapContentHeight()
|
.fillMaxWidth()
|
||||||
.padding(start = 16.dp, top = 16.dp),
|
.wrapContentHeight()
|
||||||
horizontalArrangement = Arrangement.Start,
|
.padding(start = 16.dp, top = 16.dp),
|
||||||
verticalAlignment = Alignment.Bottom
|
horizontalArrangement = Arrangement.Start,
|
||||||
) {
|
verticalAlignment = Alignment.Bottom
|
||||||
Box {
|
) {
|
||||||
TabItem(
|
Box {
|
||||||
text = stringResource(R.string.moment),
|
TabItem(
|
||||||
isSelected = pagerState.currentPage == 0,
|
text = stringResource(R.string.moment),
|
||||||
onClick = {
|
isSelected = pagerState.currentPage == 0,
|
||||||
coroutineScope.launch {
|
onClick = {
|
||||||
pagerState.animateScrollToPage(0)
|
coroutineScope.launch {
|
||||||
|
pagerState.animateScrollToPage(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
TabSpacer()
|
||||||
TabSpacer()
|
Box {
|
||||||
Box {
|
TabItem(
|
||||||
TabItem(
|
text = stringResource(R.string.users),
|
||||||
text = stringResource(R.string.users),
|
isSelected = pagerState.currentPage == 1,
|
||||||
isSelected = pagerState.currentPage == 1,
|
onClick = {
|
||||||
onClick = {
|
coroutineScope.launch {
|
||||||
coroutineScope.launch {
|
pagerState.animateScrollToPage(1)
|
||||||
pagerState.animateScrollToPage(1)
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
TabSpacer()
|
||||||
TabSpacer()
|
Box {
|
||||||
Box {
|
TabItem(
|
||||||
TabItem(
|
text = stringResource(R.string.chat_ai),
|
||||||
text = stringResource(R.string.chat_ai),
|
isSelected = pagerState.currentPage == 2,
|
||||||
isSelected = pagerState.currentPage == 2,
|
onClick = {
|
||||||
onClick = {
|
coroutineScope.launch {
|
||||||
// TODO: 实现点击逻辑
|
pagerState.animateScrollToPage(2)
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.showResult) {
|
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(
|
SearchPager(
|
||||||
pagerState = pagerState
|
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
|
@Composable
|
||||||
fun SearchInput(
|
fun SearchInput(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -300,6 +375,7 @@ fun SearchPager(
|
|||||||
when (page) {
|
when (page) {
|
||||||
0 -> MomentResultTab()
|
0 -> MomentResultTab()
|
||||||
1 -> UserResultTab()
|
1 -> UserResultTab()
|
||||||
|
2 -> AiResultTab()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -566,6 +642,118 @@ fun UserItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Composable
|
@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(
|
fun ReloadButton(
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.aiosman.ravenow.ui.index.tabs.search
|
package com.aiosman.ravenow.ui.index.tabs.search
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
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.MomentPagingSource
|
||||||
import com.aiosman.ravenow.entity.MomentRemoteDataSource
|
import com.aiosman.ravenow.entity.MomentRemoteDataSource
|
||||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
@@ -32,12 +38,37 @@ object SearchViewModel : ViewModel() {
|
|||||||
private val userService = UserServiceImpl()
|
private val userService = UserServiceImpl()
|
||||||
private val _usersFlow = MutableStateFlow<PagingData<AccountProfileEntity>>(PagingData.empty())
|
private val _usersFlow = MutableStateFlow<PagingData<AccountProfileEntity>>(PagingData.empty())
|
||||||
val usersFlow = _usersFlow.asStateFlow()
|
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 showResult by mutableStateOf(false)
|
||||||
var requestFocus 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() {
|
fun search() {
|
||||||
if (searchText.isEmpty()) {
|
if (searchText.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 记录历史
|
||||||
|
if (::historyStore.isInitialized) {
|
||||||
|
historyStore.add(searchText)
|
||||||
|
_historyFlow.value = historyStore.getHistory()
|
||||||
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
Pager(
|
Pager(
|
||||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||||
@@ -64,9 +95,34 @@ object SearchViewModel : ViewModel() {
|
|||||||
_usersFlow.value = it
|
_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
|
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){
|
suspend fun followUser(id:Int){
|
||||||
userService.followUser(id.toString())
|
userService.followUser(id.toString())
|
||||||
val currentPagingData = _usersFlow.value
|
val currentPagingData = _usersFlow.value
|
||||||
@@ -96,6 +152,7 @@ object SearchViewModel : ViewModel() {
|
|||||||
fun ResetModel(){
|
fun ResetModel(){
|
||||||
_momentsFlow.value = PagingData.empty()
|
_momentsFlow.value = PagingData.empty()
|
||||||
_usersFlow.value = PagingData.empty()
|
_usersFlow.value = PagingData.empty()
|
||||||
|
_agentsFlow.value = PagingData.empty()
|
||||||
showResult = false
|
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"
|
playServicesAuth = "21.4.0"
|
||||||
rendering = "1.17.1"
|
rendering = "1.17.1"
|
||||||
zoomable = "1.6.1"
|
zoomable = "1.6.1"
|
||||||
|
camerax = "1.3.4"
|
||||||
|
mlkitBarcode = "17.3.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" }
|
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-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
|
||||||
androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pagingRuntime" }
|
androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pagingRuntime" }
|
||||||
androidx-paging-runtime = { module = "androidx.paging:paging-runtime", 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 = { module = "io.coil-kt:coil", version.ref = "coil" }
|
||||||
coil-compose = { module = "io.coil-kt:coil-compose", 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" }
|
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" }
|
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "converterGson" }
|
||||||
zoomable = { module = "net.engawapg.lib:zoomable", version.ref = "zoomable" }
|
zoomable = { module = "net.engawapg.lib:zoomable", version.ref = "zoomable" }
|
||||||
lottie = { module="com.airbnb.android:lottie-compose", version="6.6.10"}
|
lottie = { module="com.airbnb.android:lottie-compose", version="6.6.10"}
|
||||||
|
mlkit-barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "mlkitBarcode" }
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
|
|||||||
Reference in New Issue
Block a user