新增“我的派币”功能
- **API & 数据层**: 新增积分(Points)相关的数据实体、API接口定义 (`getMyPointsBalance`, `getMyPointsChangeLogs`) 和 `PointService` 服务,用于管理和获取用户派币余额及交易记录。
- **UI & 交互**:
- 新增“我的派币”底部弹窗 (`PointsBottomSheet`),展示当前余额、累计收支、交易历史和如何赚取列表。
- 新增全局的弹窗管理器 `PointsSheetManager` 和 `PointsBottomSheetHost`,用于在应用内任何位置唤起派币弹窗。
- **功能集成**:
- 在用户个人主页的卡片上显示派币余额,并添加点击事件以打开弹窗。
- 应用启动和用户切换时刷新和清理派币数据,确保数据准确性。
This commit is contained in:
@@ -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 的通知点击
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1016,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")
|
||||||
@@ -1859,6 +1924,83 @@ 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 ==========
|
// ========== Room Member Management API ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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, // 暗色模式下为白色,亮色模式下为黑色
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user