10 Commits

Author SHA1 Message Date
2ba4c2ec02 Merge branch 'main' into atm 2025-11-05 23:15:50 +08:00
28061617da Merge pull request #49 from Kevinlinpr/zhong_1
记忆管理功能实现
2025-11-05 22:30:50 +08:00
c9e411c4ad Merge branch 'main' into zhong_1 2025-11-05 22:30:29 +08:00
995e061b6f Merge pull request #52 from Kevinlinpr/feat/pr-20251104-154907
添加深色模式缺省图
2025-11-05 22:25:09 +08:00
9c9eb66b71 新增智能体和房间规则管理功能
- 新增智能体(Agent)规则的增删改查(CRUD)和服务实现。
- 新增房间(Room)规则的增删改查(CRUD)和服务实现。
- 将 `PromptRule` 相关命名重构为 `AgentRule`,以提高代码清晰度。
- 将相关数据实体中表示总数的字段类型从 `Int` 修改为 `Long`。
2025-11-05 22:24:03 +08:00
f90bfbfa0f 添加深色模式缺省图 2025-11-05 21:31:11 +08:00
9ea03cee34 群记忆项编辑功能;群权限设置UI;群记忆管理缺省图 2025-11-05 21:29:18 +08:00
1de8bb825c Merge pull request #51 from Kevinlinpr/revert-48-feat/pr-20251104-154907
Revert "Feat/pr 20251104 154907"
2025-11-05 16:57:59 +08:00
4a1c15747c 文本资源文件替换 2025-11-04 19:01:11 +08:00
cff6b78c30 记忆管理功能实现 2025-11-04 18:28:24 +08:00
95 changed files with 3161 additions and 229 deletions

View File

@@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-09-17T06:25:35.585100400Z">
<DropdownSelection timestamp="2025-11-05T12:24:27.034893100Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=192.168.0.216:5555;connection=698a7727" />
<DeviceId pluginId="PhysicalDevice" identifier="serial=c328a150" />
</handle>
</Target>
</DropdownSelection>

View File

@@ -2,8 +2,16 @@ package com.aiosman.ravenow.data
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.AgentRule
import com.aiosman.ravenow.data.api.AgentRuleListResponse
import com.aiosman.ravenow.data.api.AgentRuleQuota
import com.aiosman.ravenow.data.api.AgentRuleAgent
import com.aiosman.ravenow.data.api.CreateAgentRuleRequestBody
import com.aiosman.ravenow.data.api.InsufficientBalanceError
import com.aiosman.ravenow.data.api.UpdateAgentRuleRequestBody
import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.entity.ProfileEntity
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
data class Agent(
@@ -83,9 +91,16 @@ data class Profile(
)
}
}
/**
* 智能体服务
*/
interface AgentService {
/**
* 获取智能体列表
* @param pageNumber 页码
* @param pageSize 每页数量,默认 20
* @param authorId 作者ID可选参数用于筛选特定作者的智能体
* @return 智能体列表容器,包含分页信息和智能体列表,失败时返回 null
*/
suspend fun getAgent(
pageNumber: Int,
@@ -96,3 +111,206 @@ interface AgentService {
}
// ========== Agent 规则 - 领域实体 ==========
data class AgentRuleAgentInfo(
val id: Int,
val title: String,
val avatar: String,
)
data class AgentRuleEntity(
val id: Int,
val rule: String,
val creator: String,
val creatorType: String,
val scope: String,
val agent: AgentRuleAgentInfo,
val createdAt: String,
val updatedAt: String,
)
data class AgentRuleListResult(
val page: Int,
val pageSize: Int,
val total: Int,
val list: List<AgentRuleEntity>,
)
data class AgentRuleQuotaEntity(
val agentId: Int,
val agentTitle: String,
val baseMaxCount: Int,
val purchasedCount: Int,
val totalMaxCount: Int,
val currentCount: Int,
val remainingCount: Int,
val usagePercent: Double,
)
// ========== Agent 规则 - Service 接口 ==========
/**
* Agent 规则服务
*/
interface AgentRuleService {
/**
* 根据 OpenId 创建 Agent 规则
* @param openId Agent 的 OpenId
* @param rule 规则内容,不能为空
* @throws ServiceException 创建失败时抛出异常(包括余额不足等情况)
*/
suspend fun createAgentRuleByOpenId(openId: String, rule: String)
/**
* 修改 Agent 规则
* @param id 规则ID
* @param rule 新的规则内容,不能为空
* @param openId Agent 的 OpenId可选参数
* @throws ServiceException 修改失败时抛出异常(包括余额不足等情况)
*/
suspend fun updateAgentRule(id: Int, rule: String, openId: String? = null)
/**
* 删除 Agent 规则
* @param id 规则ID
* @throws ServiceException 删除失败时抛出异常
*/
suspend fun deleteAgentRule(id: Int)
/**
* 查询 Agent 规则列表
* @param openId Agent 的 OpenId
* @param keyword 关键词搜索,可选参数
* @param page 页码,默认 1
* @param pageSize 每页数量,默认 10
* @return 规则列表响应,包含分页信息和规则列表
* @throws ServiceException 查询失败时抛出异常
*/
suspend fun getAgentRuleList(
openId: String,
keyword: String? = null,
page: Int = 1,
pageSize: Int = 10
): AgentRuleListResult
/**
* 查询 Agent 规则配额使用情况
* @param openId Agent 的 OpenId
* @return 规则配额信息,包含基础配额、已购买配额、当前使用量等
* @throws ServiceException 查询失败时抛出异常
*/
suspend fun getAgentRuleQuota(openId: String): AgentRuleQuotaEntity
}
class AgentRuleServiceImpl : AgentRuleService {
private val gson = Gson()
override suspend fun createAgentRuleByOpenId(openId: String, rule: String) {
val body = CreateAgentRuleRequestBody(
rule = rule,
promptId = null,
openId = openId
)
val resp = ApiClient.api.createAgentRule(body)
if (!resp.isSuccessful) {
val errorText = resp.errorBody()?.string()
try {
val err = gson.fromJson(errorText, InsufficientBalanceError::class.java)
if (err != null && err.code == 35600) {
throw ServiceException(err.message)
}
} catch (_: Exception) {
// ignore parse error
}
throw ServiceException("创建 Agent 规则失败: HTTP ${resp.code()}")
}
}
override suspend fun updateAgentRule(id: Int, rule: String, openId: String?) {
val body = UpdateAgentRuleRequestBody(
id = id,
rule = rule,
promptId = null,
openId = openId
)
val resp = ApiClient.api.updateAgentRule(body)
if (!resp.isSuccessful) {
val errorText = resp.errorBody()?.string()
try {
val err = gson.fromJson(errorText, InsufficientBalanceError::class.java)
if (err != null && err.code == 35600) {
throw ServiceException(err.message)
}
} catch (_: Exception) {
// ignore parse error
}
throw ServiceException("更新 Agent 规则失败: HTTP ${resp.code()}")
}
}
override suspend fun deleteAgentRule(id: Int) {
val resp = ApiClient.api.deleteAgentRule(id)
if (!resp.isSuccessful) {
throw ServiceException("删除 Agent 规则失败: HTTP ${resp.code()}")
}
}
override suspend fun getAgentRuleList(
openId: String,
keyword: String?,
page: Int,
pageSize: Int
): AgentRuleListResult {
val resp = ApiClient.api.getAgentRuleList(
promptId = openId,
rule = keyword,
page = page,
pageSize = pageSize
)
val data = resp.body()?.data ?: throw ServiceException("获取 Agent 规则列表失败")
return data.toEntity()
}
override suspend fun getAgentRuleQuota(openId: String): AgentRuleQuotaEntity {
val resp = ApiClient.api.getAgentRuleQuota(openId)
val data = resp.body()?.data ?: throw ServiceException("获取 Agent 规则配额失败")
return data.toEntity()
}
private fun AgentRuleListResponse.toEntity(): AgentRuleListResult = AgentRuleListResult(
page = page,
pageSize = pageSize,
total = total,
list = list.map { it.toEntity() }
)
private fun AgentRule.toEntity(): AgentRuleEntity = AgentRuleEntity(
id = id,
rule = rule,
creator = creator,
creatorType = creatorType,
scope = scope,
agent = prompt.toEntity(),
createdAt = createdAt,
updatedAt = updatedAt,
)
private fun AgentRuleAgent.toEntity(): AgentRuleAgentInfo = AgentRuleAgentInfo(
id = id,
title = title,
avatar = avatar,
)
private fun AgentRuleQuota.toEntity(): AgentRuleQuotaEntity = AgentRuleQuotaEntity(
agentId = promptId,
agentTitle = promptTitle,
baseMaxCount = baseMaxCount,
purchasedCount = purchasedCount,
totalMaxCount = totalMaxCount,
currentCount = currentCount,
remainingCount = remainingCount,
usagePercent = usagePercent,
)
}

View File

@@ -9,7 +9,7 @@ import com.google.gson.annotations.SerializedName
data class ListContainer<T>(
// 总数
@SerializedName("total")
val total: Int,
val total: Long,
// 当前页
@SerializedName("page")
val page: Int,

View File

@@ -1,11 +1,16 @@
package com.aiosman.ravenow.data
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.data.api.CreateRoomRuleRequestBody
import com.aiosman.ravenow.data.api.UpdateRoomRuleRequestBody
import com.aiosman.ravenow.data.api.RoomRuleQuota
import com.aiosman.ravenow.data.api.RoomRule
import com.aiosman.ravenow.data.api.RoomRuleCreator
import com.aiosman.ravenow.entity.CreatorEntity
import com.aiosman.ravenow.entity.ProfileEntity
import com.aiosman.ravenow.entity.RoomEntity
import com.aiosman.ravenow.entity.RoomRuleEntity
import com.aiosman.ravenow.entity.RoomRuleCreatorEntity
import com.aiosman.ravenow.entity.RoomRuleQuotaEntity
import com.aiosman.ravenow.entity.UsersEntity
import com.google.gson.annotations.SerializedName
@@ -106,6 +111,190 @@ data class Users(
}
}
/**
* 房间规则相关服务
*/
interface RoomService {
/**
* 创建房间规则
* @param rule 规则内容,不能为空
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID与 roomId 二选一
* @throws ServiceException 创建失败时抛出异常
*/
suspend fun createRoomRule(
rule: String,
roomId: Int? = null,
trtcId: String? = null
)
/**
* 修改房间规则
* @param id 规则ID
* @param rule 新的规则内容,不能为空
* @throws ServiceException 修改失败时抛出异常
*/
suspend fun updateRoomRule(
id: Int,
rule: String
)
/**
* 删除房间规则
* @param id 规则ID
* @throws ServiceException 删除失败时抛出异常
*/
suspend fun deleteRoomRule(id: Int)
/**
* 查询房间规则列表
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID与 roomId 二选一
* @param page 页码,默认 1
* @param pageSize 每页数量,默认 10
* @return 规则列表响应,包含分页信息和规则列表
* @throws ServiceException 查询失败时抛出异常
*/
suspend fun getRoomRuleList(
roomId: Int? = null,
trtcId: String? = null,
page: Int = 1,
pageSize: Int = 10
): ListContainer<RoomRuleEntity>
/**
* 查询规则配额使用情况
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID与 roomId 二选一
* @return 规则配额信息
* @throws ServiceException 查询失败时抛出异常
*/
suspend fun getRoomRuleQuota(
roomId: Int? = null,
trtcId: String? = null
): RoomRuleQuotaEntity
}
/**
* 房间规则服务实现类
*/
class RoomServiceImpl : RoomService {
override suspend fun createRoomRule(
rule: String,
roomId: Int?,
trtcId: String?
) {
val resp = ApiClient.api.createRoomRule(
CreateRoomRuleRequestBody(
rule = rule,
roomId = roomId,
trtcId = trtcId
)
)
if (!resp.isSuccessful) {
throw ServiceException("创建房间规则失败")
}
}
override suspend fun updateRoomRule(
id: Int,
rule: String
) {
val resp = ApiClient.api.updateRoomRule(
UpdateRoomRuleRequestBody(
id = id,
rule = rule
)
)
if (!resp.isSuccessful) {
throw ServiceException("修改房间规则失败")
}
}
override suspend fun deleteRoomRule(id: Int) {
val resp = ApiClient.api.deleteRoomRule(id)
if (!resp.isSuccessful) {
throw ServiceException("删除房间规则失败")
}
}
override suspend fun getRoomRuleList(
roomId: Int?,
trtcId: String?,
page: Int,
pageSize: Int
): ListContainer<RoomRuleEntity> {
val resp = ApiClient.api.getRoomRuleList(
roomId = roomId,
trtcId = trtcId,
page = page,
pageSize = pageSize
)
val body = resp.body() ?: throw ServiceException("获取房间规则列表失败")
return ListContainer(
list = body.list.map { it.toRoomRuleEntity() },
page = body.page,
total = body.total,
pageSize = body.pageSize
)
}
override suspend fun getRoomRuleQuota(
roomId: Int?,
trtcId: String?
): RoomRuleQuotaEntity {
val resp = ApiClient.api.getRoomRuleQuota(
roomId = roomId,
trtcId = trtcId
)
val body = resp.body() ?: throw ServiceException("获取规则配额信息失败")
val data = body.data ?: throw ServiceException("规则配额数据为空")
return data.toRoomRuleQuotaEntity()
}
}
/**
* RoomRule 扩展函数,转换为 RoomRuleEntity
*/
fun RoomRule.toRoomRuleEntity(): RoomRuleEntity {
return RoomRuleEntity(
id = id,
rule = rule,
creator = creator?.toRoomRuleCreatorEntity(),
creatorType = creatorType,
roomId = roomId,
createdAt = createdAt,
updatedAt = updatedAt
)
}
/**
* RoomRuleCreator 扩展函数,转换为 RoomRuleCreatorEntity
*/
fun RoomRuleCreator.toRoomRuleCreatorEntity(): RoomRuleCreatorEntity {
return RoomRuleCreatorEntity(
id = id,
nickname = nickname,
avatar = avatar
)
}
/**
* RoomRuleQuota 扩展函数,转换为 RoomRuleQuotaEntity
*/
fun RoomRuleQuota.toRoomRuleQuotaEntity(): RoomRuleQuotaEntity {
return RoomRuleQuotaEntity(
baseMaxCount = baseMaxCount,
purchasedCount = purchasedCount,
totalMaxCount = totalMaxCount,
currentCount = currentCount,
remainingCount = remainingCount,
usagePercent = usagePercent
)
}

View File

@@ -489,7 +489,7 @@ data class CategoryListResponse(
val list: List<CategoryTemplate>
)
// ========== Prompt Rule 相关数据类 ==========
// ========== Agent Rule 相关数据类 ==========
/**
* 创建规则请求体
@@ -497,7 +497,7 @@ data class CategoryListResponse(
* @param promptId 智能体ID与 openId 二选一promptId 优先
* @param openId 智能体的 OpenIDUUID格式与 promptId 二选一
*/
data class CreatePromptRuleRequestBody(
data class CreateAgentRuleRequestBody(
@SerializedName("rule")
val rule: String,
@SerializedName("promptId")
@@ -513,7 +513,7 @@ data class CreatePromptRuleRequestBody(
* @param promptId 要更改关联的智能体ID可选
* @param openId 要更改关联的智能体 OpenID可选
*/
data class UpdatePromptRuleRequestBody(
data class UpdateAgentRuleRequestBody(
@SerializedName("id")
val id: Int,
@SerializedName("rule")
@@ -530,7 +530,7 @@ data class UpdatePromptRuleRequestBody(
* @param title 智能体标题
* @param avatar 智能体头像URL
*/
data class PromptRuleAgent(
data class AgentRuleAgent(
@SerializedName("id")
val id: Int,
@SerializedName("title")
@@ -550,7 +550,7 @@ data class PromptRuleAgent(
* @param createdAt 创建时间ISO 8601 格式)
* @param updatedAt 更新时间ISO 8601 格式)
*/
data class PromptRule(
data class AgentRule(
@SerializedName("id")
val id: Int,
@SerializedName("rule")
@@ -562,7 +562,7 @@ data class PromptRule(
@SerializedName("scope")
val scope: String,
@SerializedName("prompt")
val prompt: PromptRuleAgent,
val prompt: AgentRuleAgent,
@SerializedName("created_at")
val createdAt: String,
@SerializedName("updated_at")
@@ -576,7 +576,7 @@ data class PromptRule(
* @param total 总记录数
* @param list 规则列表
*/
data class PromptRuleListResponse(
data class AgentRuleListResponse(
@SerializedName("page")
val page: Int,
@SerializedName("pageSize")
@@ -584,7 +584,7 @@ data class PromptRuleListResponse(
@SerializedName("total")
val total: Int,
@SerializedName("list")
val list: List<PromptRule>
val list: List<AgentRule>
)
/**
@@ -598,7 +598,7 @@ data class PromptRuleListResponse(
* @param remainingCount 剩余可用条数
* @param usagePercent 使用百分比0-100
*/
data class PromptRuleQuota(
data class AgentRuleQuota(
@SerializedName("promptId")
val promptId: Int,
@SerializedName("promptTitle")
@@ -617,6 +617,150 @@ data class PromptRuleQuota(
val usagePercent: Double
)
// ========== Room Rule 相关数据类 ==========
/**
* 创建房间规则请求体
* @param rule 规则内容,不能为空
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID字符串格式与 roomId 二选一
*/
data class CreateRoomRuleRequestBody(
@SerializedName("rule")
val rule: String,
@SerializedName("roomId")
val roomId: Int? = null,
@SerializedName("trtcId")
val trtcId: String? = null
)
/**
* 修改房间规则请求体
* @param id 规则ID必填
* @param rule 新的规则内容,不能为空
*/
data class UpdateRoomRuleRequestBody(
@SerializedName("id")
val id: Int,
@SerializedName("rule")
val rule: String
)
/**
* 房间规则创建者信息
* @param id 创建者ID
* @param nickname 创建者昵称
* @param avatar 创建者头像文件名
*/
data class RoomRuleCreator(
@SerializedName("id")
val id: Int,
@SerializedName("nickname")
val nickname: String,
@SerializedName("avatar")
val avatar: String
)
/**
* 房间规则详情
* @param id 规则ID
* @param rule 规则内容
* @param creator 创建者信息(可能为 null
* @param creatorType 创建者类型(如 "user"
* @param roomId 所属房间ID
* @param createdAt 创建时间ISO 8601 格式)
* @param updatedAt 更新时间ISO 8601 格式)
*/
data class RoomRule(
@SerializedName("id")
val id: Int,
@SerializedName("rule")
val rule: String,
@SerializedName("creator")
val creator: RoomRuleCreator?,
@SerializedName("creatorType")
val creatorType: String,
@SerializedName("roomId")
val roomId: Int,
@SerializedName("createdAt")
val createdAt: String,
@SerializedName("updatedAt")
val updatedAt: String
)
/**
* 房间规则列表响应
* @param page 当前页码
* @param pageSize 每页数量
* @param total 总记录数
* @param list 规则列表
*/
data class RoomRuleListResponse(
@SerializedName("page")
val page: Int,
@SerializedName("pageSize")
val pageSize: Int,
@SerializedName("total")
val total: Long,
@SerializedName("list")
val list: List<RoomRule>
)
/**
* 房间规则配额信息
* @param baseMaxCount 基础条数限制(系统配置的免费配额)
* @param purchasedCount 用户购买的额外条数(通过自动扩容机制)
* @param totalMaxCount 总可用条数baseMaxCount + purchasedCount
* @param currentCount 当前已创建的规则条数(该用户在该房间)
* @param remainingCount 剩余可用条数totalMaxCount - currentCount
* @param usagePercent 使用百分比0-100
*/
data class RoomRuleQuota(
@SerializedName("baseMaxCount")
val baseMaxCount: Int,
@SerializedName("purchasedCount")
val purchasedCount: Int,
@SerializedName("totalMaxCount")
val totalMaxCount: Int,
@SerializedName("currentCount")
val currentCount: Int,
@SerializedName("remainingCount")
val remainingCount: Int,
@SerializedName("usagePercent")
val usagePercent: Double
)
/**
* 积分不足错误响应
*
* 当创建房间规则时积分不足,服务器会返回此特殊错误格式。
* 包含当前余额和所需金额信息,便于客户端提示用户。
*
* @param success 是否成功,固定为 false
* @param code 错误码,固定为 35600积分不足
* @param message 错误消息,包含所需积分和当前余额的详细说明
* @param err 错误类型,固定为 "InsufficientBalance"
* @param currentBalance 当前积分余额
* @param requiredAmount 所需积分数量
* @param traceId 追踪ID可选
*/
data class InsufficientBalanceError(
@SerializedName("success")
val success: Boolean,
@SerializedName("code")
val code: Int,
@SerializedName("message")
val message: String,
@SerializedName("err")
val err: String,
@SerializedName("currentBalance")
val currentBalance: Int,
@SerializedName("requiredAmount")
val requiredAmount: Int,
@SerializedName("traceId")
val traceId: String?
)
interface RaveNowAPI {
@GET("membership/config")
@retrofit2.http.Headers("X-Requires-Auth: true")
@@ -992,7 +1136,7 @@ interface RaveNowAPI {
@Query("pageSize") pageSize: Int? = null
): Response<ListContainer<Agent>>
// ========== Prompt Rule API ==========
// ========== Agent Rule API ==========
/**
* 创建智能体规则
@@ -1020,8 +1164,8 @@ interface RaveNowAPI {
* ```
*/
@POST("outside/prompt/rule")
suspend fun createPromptRule(
@Body body: CreatePromptRuleRequestBody
suspend fun createAgentRule(
@Body body: CreateAgentRuleRequestBody
): Response<Unit>
/**
@@ -1054,8 +1198,8 @@ interface RaveNowAPI {
* ```
*/
@retrofit2.http.PUT("outside/prompt/rule")
suspend fun updatePromptRule(
@Body body: UpdatePromptRuleRequestBody
suspend fun updateAgentRule(
@Body body: UpdateAgentRuleRequestBody
): Response<Unit>
/**
@@ -1079,7 +1223,7 @@ interface RaveNowAPI {
* ```
*/
@DELETE("outside/prompt/rule/{id}")
suspend fun deletePromptRule(
suspend fun deleteAgentRule(
@Path("id") id: Int
): Response<Unit>
@@ -1134,12 +1278,12 @@ interface RaveNowAPI {
* ```
*/
@GET("outside/prompt/{promptId}/rule/list")
suspend fun getPromptRuleList(
suspend fun getAgentRuleList(
@Path("promptId") promptId: String,
@Query("rule") rule: String? = null,
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 10
): Response<DataContainer<PromptRuleListResponse>>
): Response<DataContainer<AgentRuleListResponse>>
/**
* 查询智能体规则配额信息
@@ -1189,9 +1333,264 @@ interface RaveNowAPI {
* ```
*/
@GET("outside/prompt/{promptId}/rule/count")
suspend fun getPromptRuleQuota(
suspend fun getAgentRuleQuota(
@Path("promptId") promptId: String
): Response<DataContainer<PromptRuleQuota>>
): Response<DataContainer<AgentRuleQuota>>
// ========== Room Rule API ==========
/**
* 创建房间规则
*
* 功能说明:
* - 为指定的房间创建一条新规则
* - 规则会被添加到AI对话上下文中用于约束房间内的行为和交互方式
* - 必须是房间成员或房间创建者才能创建规则
* - 每个用户在每个房间有基础条数限制,达到限制后会自动扣费扩容
* - 如果积分不足,会返回特殊的错误响应(错误码 35600
*
* @param body 创建规则请求体
* - rule: 规则内容,不能为空
* - roomId: 房间ID与 trtcId 二选一)
* - trtcId: TRTC房间ID与 roomId 二选一)
*
* @return 成功时返回空 Unit失败时返回错误信息
*
* 错误响应说明:
* - 400 (40001): 缺少房间标识(必须提供 roomId 或 trtcId
* - 400 (40002): 规则内容为空
* - 400 (40003): 规则内容超出字数限制
* - 400 (35600): 积分不足(自动扩容失败),返回 InsufficientBalanceError 格式
* - 401 (40101): 未登录
* - 403 (40301): 无权限为该房间创建规则
* - 400 (40005): 房间不存在
*
* 示例:
* ```kotlin
* // 使用 roomId 创建规则
* val request1 = CreateRoomRuleRequestBody(
* rule = "禁止在房间内讨论政治话题",
* roomId = 123
* )
* val response1 = api.createRoomRule(request1)
*
* // 使用 trtcId 创建规则
* val request2 = CreateRoomRuleRequestBody(
* rule = "请使用文明用语",
* trtcId = "room_trtc_123"
* )
* val response2 = api.createRoomRule(request2)
*
* // 处理积分不足错误
* if (!response1.isSuccessful && response1.code() == 400) {
* val errorBody = response1.errorBody()?.string()
* // 解析为 InsufficientBalanceError 获取余额信息
* }
* ```
*/
@POST("outside/room/rule")
suspend fun createRoomRule(
@Body body: CreateRoomRuleRequestBody
): Response<Unit>
/**
* 修改房间规则
*
* 功能说明:
* - 修改指定的房间规则内容
* - 只能修改自己创建的规则creator_type 为 "user"
* - 修改不会增加规则条数,因此不会触发扣费
*
* @param body 修改规则请求体
* - id: 规则ID必填
* - rule: 新的规则内容,不能为空
*
* @return 成功时返回空 Unit失败时返回错误信息
*
* 权限要求:
* - 必须是规则的创建者
* - 规则必须是用户类型creator_type = "user"
*
* 错误响应说明:
* - 400 (40002): 规则内容为空
* - 400 (40003): 规则内容超出字数限制
* - 401 (40101): 未登录
* - 403 (40301): 无权限修改该规则
* - 404 (40401): 规则不存在
*
* 示例:
* ```kotlin
* val request = UpdateRoomRuleRequestBody(
* id = 456,
* rule = "请保持友善交流"
* )
* val response = api.updateRoomRule(request)
* ```
*/
@retrofit2.http.PUT("outside/room/rule")
suspend fun updateRoomRule(
@Body body: UpdateRoomRuleRequestBody
): Response<Unit>
/**
* 删除房间规则
*
* 功能说明:
* - 删除指定的房间规则
* - 只能删除自己创建的规则creator_type 为 "user"
* - 删除操作不可恢复,请谨慎操作
* - 删除规则不会退还已扣除的积分
*
* @param id 规则ID
*
* @return 成功时返回空 Unit失败时返回错误信息
*
* 权限要求:
* - 必须是规则的创建者
* - 规则必须是用户类型creator_type = "user"
*
* 错误响应说明:
* - 400 (40006): 无效的规则ID
* - 401 (40101): 未登录
* - 403 (40301): 无权限删除该规则
*
* 示例:
* ```kotlin
* val response = api.deleteRoomRule(456)
* ```
*/
@DELETE("outside/room/rule/{id}")
suspend fun deleteRoomRule(
@Path("id") id: Int
): Response<Unit>
/**
* 查询房间规则列表
*
* 功能说明:
* - 查询指定房间的规则列表,支持分页
* - 返回该房间所有用户创建的规则,包含创建者信息
* - 必须是房间成员或房间创建者才能查询
*
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID与 roomId 二选一
* @param page 页码,默认 1
* @param pageSize 每页数量,默认 10
*
* @return 返回分页的规则列表,包含规则详情和创建者信息
*
* 响应数据说明:
* - page: 当前页码
* - pageSize: 每页数量
* - total: 总记录数
* - list: 规则列表
* - id: 规则ID
* - rule: 规则内容
* - creator: 创建者信息id, nickname, avatar
* - creatorType: 创建者类型("user" 等)
* - roomId: 所属房间ID
* - createdAt: 创建时间ISO 8601格式
* - updatedAt: 更新时间ISO 8601格式
*
* 错误响应说明:
* - 400 (40001): 缺少房间标识(必须提供 roomId 或 trtcId
* - 401 (40101): 未登录
* - 403 (40301): 无权限查看该房间的规则
*
* 示例:
* ```kotlin
* // 使用 roomId 查询
* val response1 = api.getRoomRuleList(
* roomId = 123,
* page = 1,
* pageSize = 10
* )
*
* // 使用 trtcId 查询
* val response2 = api.getRoomRuleList(
* trtcId = "room_trtc_123",
* page = 1,
* pageSize = 10
* )
*
* // 处理响应
* response1.body()?.let { result ->
* println("共 ${result.total} 条规则,当前第 ${result.page} 页")
* result.list.forEach { rule ->
* println("规则: ${rule.rule}, 创建者: ${rule.creator?.nickname}")
* }
* }
* ```
*/
@GET("outside/room/rule")
suspend fun getRoomRuleList(
@Query("roomId") roomId: Int? = null,
@Query("trtcId") trtcId: String? = null,
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 10
): Response<RoomRuleListResponse>
/**
* 查询规则额度使用情况
*
* 功能说明:
* - 查询当前用户在指定房间的规则额度使用情况
* - 包括基础条数、已购买条数、当前使用数等完整信息
* - 用于判断用户是否还能创建新规则
* - 必须是房间成员或房间创建者才能查询
*
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID与 roomId 二选一
*
* @return 返回配额详细信息
*
* 响应数据说明:
* - baseMaxCount: 基础条数限制(系统配置的免费配额)
* - purchasedCount: 用户购买的额外条数(通过自动扩容机制)
* - totalMaxCount: 总可用条数baseMaxCount + purchasedCount
* - currentCount: 当前已创建的规则条数(该用户在该房间)
* - remainingCount: 剩余可用条数totalMaxCount - currentCount
* - usagePercent: 使用百分比0-100currentCount / totalMaxCount * 100
*
* 使用场景:
* 1. 创建规则前检查是否有足够配额
* 2. 展示规则使用情况统计
* 3. 提示用户即将达到配额上限
*
* 错误响应说明:
* - 400 (40001): 缺少房间标识(必须提供 roomId 或 trtcId
* - 401 (40101): 未登录
* - 403 (40301): 无权限查看该房间的规则条数信息
*
* 示例:
* ```kotlin
* // 使用 roomId 查询
* val response1 = api.getRoomRuleQuota(roomId = 123)
*
* // 使用 trtcId 查询
* val response2 = api.getRoomRuleQuota(trtcId = "room_trtc_123")
*
* // 处理响应
* response1.body()?.data?.let { quota ->
* if (quota.remainingCount > 0) {
* // 可以创建新规则
* println("还可以创建 ${quota.remainingCount} 条规则")
* println("使用率: ${quota.usagePercent}%")
* } else {
* // 配额已用完,需要扩容
* println("规则配额已用完,已使用 ${quota.currentCount}/${quota.totalMaxCount}")
* println("创建新规则将自动扣除积分进行扩容")
* }
* }
* ```
*/
@GET("outside/room/rule/length-info")
suspend fun getRoomRuleQuota(
@Query("roomId") roomId: Int? = null,
@Query("trtcId") trtcId: String? = null
): Response<DataContainer<RoomRuleQuota>>
}

View File

@@ -7,18 +7,13 @@ import com.aiosman.ravenow.data.Agent
import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.AgentService
import com.aiosman.ravenow.data.DataContainer
import com.aiosman.ravenow.data.MomentService
import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.UploadImage
import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.api.ApiClient
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.http.Part
import java.io.File
import java.io.IOException
@@ -160,7 +155,7 @@ class AgentBackend {
return if (authorId != null) {
// getAgent 返回 DataContainer<ListContainer<Agent>>
val dataContainer =
body as com.aiosman.ravenow.data.DataContainer<com.aiosman.ravenow.data.ListContainer<com.aiosman.ravenow.data.Agent>>
body as DataContainer<ListContainer<Agent>>
val listContainer = dataContainer.data
ListContainer(
total = listContainer.total,
@@ -171,7 +166,7 @@ class AgentBackend {
} else {
// getMyAgent 返回 ListContainer<Agent>
val listContainer =
body as com.aiosman.ravenow.data.ListContainer<com.aiosman.ravenow.data.Agent>
body as ListContainer<Agent>
ListContainer(
total = listContainer.total,
page = pageNumber,
@@ -251,7 +246,7 @@ class AgentLoader : DataLoader<AgentEntity, AgentLoaderExtraArgs>() {
} else {
// getMyAgent 返回 ListContainer<Agent>
val listContainer =
body as com.aiosman.ravenow.data.ListContainer<com.aiosman.ravenow.data.Agent>
body as ListContainer<Agent>
ListContainer(
list = listContainer.list.map { it.toAgentEntity() },
total = listContainer.total,

View File

@@ -9,7 +9,7 @@ import com.aiosman.ravenow.data.ListContainer
abstract class DataLoader<T,ET> {
var list: MutableList<T> = mutableListOf()
var page by mutableStateOf(1)
var total by mutableStateOf(0)
var total by mutableStateOf(0L)
var pageSize by mutableStateOf(10)
var hasNext by mutableStateOf(true)
var isLoading by mutableStateOf(false)

View File

@@ -3,10 +3,6 @@ package com.aiosman.ravenow.entity
import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.api.ApiClient
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
/**
* 群聊房间
@@ -56,6 +52,40 @@ data class ProfileEntity(
val aiAccount: Boolean,
)
/**
* 房间规则创建者信息
*/
data class RoomRuleCreatorEntity(
val id: Int,
val nickname: String,
val avatar: String
)
/**
* 房间规则详情
*/
data class RoomRuleEntity(
val id: Int,
val rule: String,
val creator: RoomRuleCreatorEntity?,
val creatorType: String,
val roomId: Int,
val createdAt: String,
val updatedAt: String
)
/**
* 房间规则配额信息
*/
data class RoomRuleQuotaEntity(
val baseMaxCount: Int,
val purchasedCount: Int,
val totalMaxCount: Int,
val currentCount: Int,
val remainingCount: Int,
val usagePercent: Double
)
class RoomLoader : DataLoader<AgentEntity,AgentLoaderExtraArgs>() {
override suspend fun fetchData(
page: Int,

View File

@@ -71,4 +71,29 @@ object AppStore {
AppState.chatBackgroundUrl = url
}
// ===================== 用户本地扩展信息 =====================
// 后端暂未提供 MBTI 与星座字段,使用本地持久化按用户维度进行存储
private fun mbtiKey(userId: Int) = "mbti_user_$userId"
private fun zodiacKey(userId: Int) = "zodiac_user_$userId"
fun getUserMbti(userId: Int): String? {
return sharedPreferences.getString(mbtiKey(userId), null)
}
fun setUserMbti(userId: Int, mbti: String?) {
sharedPreferences.edit().apply {
if (mbti.isNullOrEmpty()) remove(mbtiKey(userId)) else putString(mbtiKey(userId), mbti)
}.apply()
}
fun getUserZodiac(userId: Int): String? {
return sharedPreferences.getString(zodiacKey(userId), null)
}
fun setUserZodiac(userId: Int, zodiac: String?) {
sharedPreferences.edit().apply {
if (zodiac.isNullOrEmpty()) remove(zodiacKey(userId)) else putString(zodiacKey(userId), zodiac)
}.apply()
}
}

View File

@@ -35,8 +35,10 @@ import com.aiosman.ravenow.LocalSharedTransitionScope
import com.aiosman.ravenow.ui.about.AboutScreen
import com.aiosman.ravenow.ui.account.AccountEditScreen2
import com.aiosman.ravenow.ui.account.AccountSetting
import com.aiosman.ravenow.ui.account.MbtiSelectScreen
import com.aiosman.ravenow.ui.account.RemoveAccountScreen
import com.aiosman.ravenow.ui.account.ResetPasswordScreen
import com.aiosman.ravenow.ui.account.ZodiacSelectScreen
import com.aiosman.ravenow.ui.agent.AddAgentScreen
import com.aiosman.ravenow.ui.agent.AgentImageCropScreen
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
@@ -120,6 +122,8 @@ sealed class NavigationRoute(
data object VipSelPage : NavigationRoute("VipSelPage")
data object RemoveAccountScreen: NavigationRoute("RemoveAccount")
data object NotificationScreen : NavigationRoute("NotificationScreen")
data object MbtiSelect : NavigationRoute("MbtiSelect")
data object ZodiacSelect : NavigationRoute("ZodiacSelect")
}
@@ -421,6 +425,12 @@ fun NavigationController(
composable(route = NavigationRoute.RemoveAccountScreen.route) {
RemoveAccountScreen()
}
composable(route = NavigationRoute.MbtiSelect.route) {
MbtiSelectScreen()
}
composable(route = NavigationRoute.ZodiacSelect.route) {
ZodiacSelectScreen()
}
composable(route = NavigationRoute.VipSelPage.route) {
VipSelPage()
}

View File

@@ -26,6 +26,9 @@ object AccountEditViewModel : ViewModel() {
var croppedBitmap by mutableStateOf<Bitmap?>(null)
var isUpdating by mutableStateOf(false)
var isLoading by mutableStateOf(false)
// 本地扩展字段
var mbti by mutableStateOf<String?>(null)
var zodiac by mutableStateOf<String?>(null)
suspend fun reloadProfile(updateTrtcProfile:Boolean = false) {
Log.d("AccountEditViewModel", "reloadProfile: 开始加载用户资料")
isLoading = true
@@ -38,6 +41,12 @@ object AccountEditViewModel : ViewModel() {
bio = it.bio
// 清除之前裁剪的图片
croppedBitmap = null
// 读取本地扩展字段
try {
val uid = it.id // 使用 profile 的 id确保非空
mbti = com.aiosman.ravenow.AppStore.getUserMbti(uid)
zodiac = com.aiosman.ravenow.AppStore.getUserZodiac(uid)
} catch (_: Exception) { }
if (updateTrtcProfile) {
TrtcHelper.updateTrtcProfile(
it.nickName,
@@ -84,6 +93,13 @@ object AccountEditViewModel : ViewModel() {
nickName = newName,
bio = cleanBio
)
// 保存本地扩展字段
try {
profile?.id?.let { uid ->
com.aiosman.ravenow.AppStore.setUserMbti(uid, mbti)
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiac)
}
} catch (_: Exception) { }
// 刷新用户资料
reloadProfile()
// 刷新个人资料页面的用户资料

View File

@@ -0,0 +1,136 @@
package com.aiosman.ravenow.ui.account
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.fillMaxSize
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.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
// MBTI类型列表
val MBTI_TYPES = listOf(
"INTJ", "INTP", "ENTJ", "ENTP",
"INFJ", "INFP", "ENFJ", "ENFP",
"ISTJ", "ISFJ", "ESTJ", "ESFJ",
"ISTP", "ISFP", "ESTP", "ESFP"
)
@Composable
fun MbtiSelectScreen() {
val navController = LocalNavController.current
val appColors = LocalAppTheme.current
val model = AccountEditViewModel
val currentMbti = model.mbti
Column(
modifier = Modifier
.fillMaxSize()
.background(appColors.profileBackground)
) {
// 头部
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
NoticeScreenHeader(
title = stringResource(R.string.choose_mbti),
moreIcon = false
)
}
// 列表
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
items(MBTI_TYPES) { mbti ->
MBTIItem(
mbti = mbti,
isSelected = mbti == currentMbti,
onClick = {
model.mbti = mbti
navController.navigateUp()
}
)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
@Composable
fun MBTIItem(
mbti: String,
isSelected: Boolean,
onClick: () -> Unit
) {
val appColors = LocalAppTheme.current
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(if (isSelected) appColors.main.copy(alpha = 0.1f) else Color.White)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
onClick()
}
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = mbti,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = if (isSelected) appColors.main else appColors.text,
modifier = Modifier.weight(1f)
)
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
modifier = Modifier.size(20.dp),
tint = appColors.main
)
}
}
}
}

View File

@@ -0,0 +1,132 @@
package com.aiosman.ravenow.ui.account
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.fillMaxSize
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
// 星座列表
val ZODIAC_SIGNS = listOf(
"白羊座", "金牛座", "双子座", "巨蟹座",
"狮子座", "处女座", "天秤座", "天蝎座",
"射手座", "摩羯座", "水瓶座", "双鱼座"
)
@Composable
fun ZodiacSelectScreen() {
val navController = LocalNavController.current
val appColors = LocalAppTheme.current
val model = AccountEditViewModel
val currentZodiac = model.zodiac
Column(
modifier = Modifier
.fillMaxSize()
.background(appColors.profileBackground)
) {
// 头部
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
NoticeScreenHeader(
title = stringResource(R.string.choose_zodiac),
moreIcon = false
)
}
// 列表
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
items(ZODIAC_SIGNS) { zodiac ->
ZodiacItem(
zodiac = zodiac,
isSelected = zodiac == currentZodiac,
onClick = {
model.zodiac = zodiac
navController.navigateUp()
}
)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
@Composable
fun ZodiacItem(
zodiac: String,
isSelected: Boolean,
onClick: () -> Unit
) {
val appColors = LocalAppTheme.current
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(if (isSelected) appColors.main.copy(alpha = 0.1f) else Color.White)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
onClick()
}
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = zodiac,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = if (isSelected) appColors.main else appColors.text,
modifier = Modifier.weight(1f)
)
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
modifier = Modifier.size(20.dp),
tint = appColors.main
)
}
}
}
}

View File

@@ -125,12 +125,13 @@ fun CommentNoticeScreen() {
) {
androidx.compose.foundation.Image(
painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_pl_qs_as_img
id = if(AppState.darkMode) R.mipmap.tietie_dark
else R.mipmap.invalid_name_11),
contentDescription = "No Comment",
modifier = Modifier.size(181.dp)
modifier = Modifier
.size(width = 181.dp, height = 153.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "等一位旅人~",
color = AppColors.text,

View File

@@ -137,9 +137,9 @@ fun FavouriteListPage() {
id = if (com.aiosman.ravenow.AppState.darkMode) R.mipmap.invalid_dark
else R.mipmap.invalid_name_1),
contentDescription = "No favourites",
modifier = Modifier.size(110.dp)
modifier = Modifier.size(181.dp, 153.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
Text(
text = stringResource(R.string.favourites_null),
color = AppColors.text,

View File

@@ -124,21 +124,21 @@ fun FollowerListScreen(userId: Int) {
) {
Image(
painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_fs_qs_as_img
id = if(AppState.darkMode) R.mipmap.frame_4
else R.mipmap.invalid_name_8),
contentDescription = null,
modifier = Modifier.size(181.dp)
modifier = Modifier.size(181.dp, 153.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
androidx.compose.material.Text(
text = "还没有人关注",
text = "还没有人关注你呢",
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = "去发布动态,吸引更多粉丝",
text = "试着发信号出来,某人就会被吸引啦",
color = appColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400

View File

@@ -114,21 +114,22 @@ fun FollowerNoticeScreen() {
) {
Image(
painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_fs_qs_as_img
id = if(AppState.darkMode) R.mipmap.frame_4
else R.mipmap.invalid_name_8),
contentDescription = "No Followers",
modifier = Modifier.size(181.dp)
modifier = Modifier
.size(width = 181.dp, height = 153.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
androidx.compose.material.Text(
text = "还没有人关注",
text = "还没有人关注你呢",
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = "去发布动态,吸引更多粉丝",
text = "试着发信号出来,某人就会被吸引啦",
color = AppColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400

View File

@@ -126,14 +126,14 @@ fun FollowingListScreen(userId: Int) {
) {
Image(
painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_gz_qs_as_img_my
id = if(AppState.darkMode) R.mipmap.frame_3
else R.mipmap.invalid_name_9),
contentDescription = null,
modifier = Modifier.size(181.dp)
modifier = Modifier.size(181.dp, 153.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
androidx.compose.material.Text(
text = "没有关注任何灵魂",
text = "没有关注任何灵魂",
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600

View File

@@ -1,5 +1,6 @@
package com.aiosman.ravenow.ui.group
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -8,6 +9,10 @@ import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.ChatState
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.CreatePromptRuleRequestBody
import com.aiosman.ravenow.data.api.PromptRule
import com.aiosman.ravenow.data.api.PromptRuleQuota
import com.aiosman.ravenow.data.parseErrorResponse
import com.aiosman.ravenow.entity.ChatNotification
import com.aiosman.ravenow.entity.GroupInfo
import com.aiosman.ravenow.entity.GroupMember
@@ -22,9 +27,36 @@ class GroupChatInfoViewModel(
var isLoading by mutableStateOf(false)
var error by mutableStateOf<String?>(null)
var chatNotification by mutableStateOf<ChatNotification?>(null)
var isAddingMemory by mutableStateOf(false)
var addMemoryError by mutableStateOf<String?>(null)
var addMemorySuccess by mutableStateOf(false)
val notificationStrategy get() = chatNotification?.strategy ?: "default"
// 记忆管理相关状态
var memoryQuota by mutableStateOf<PromptRuleQuota?>(null)
var memoryList by mutableStateOf<List<PromptRule>>(emptyList())
var isLoadingMemory by mutableStateOf(false)
var memoryError by mutableStateOf<String?>(null)
var promptOpenId by mutableStateOf<String?>(null)
init {
loadGroupInfo()
loadPromptOpenId()
}
/**
* 获取群聊中智能体的 OpenID
*/
private fun loadPromptOpenId() {
viewModelScope.launch {
try {
val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId)
val groupChatResponse = response.body()?.data
val prompts = groupChatResponse?.prompts
promptOpenId = prompts?.firstOrNull()?.openId
} catch (e: Exception) {
Log.e("GroupChatInfoViewModel", "获取智能体OpenID失败: ${e.message}", e)
}
}
}
suspend fun updateNotificationStrategy(strategy: String) {
val result = ChatState.updateChatNotification(groupId.hashCode(), strategy)
@@ -71,4 +103,218 @@ class GroupChatInfoViewModel(
}
}
}
/**
* 添加群记忆
* @param memoryText 记忆内容
* @param promptOpenId 智能体的 OpenID可选如果不提供则从群聊信息中获取
*/
fun addGroupMemory(memoryText: String, promptOpenId: String? = null) {
viewModelScope.launch {
try {
isAddingMemory = true
addMemoryError = null
addMemorySuccess = false
// 如果没有提供 promptOpenId需要先获取群聊的智能体信息
val openId = promptOpenId ?: run {
// 通过 createGroupChatAi 接口获取群聊详细信息(包含 prompts
val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId)
val groupChatResponse = response.body()?.data
val prompts = groupChatResponse?.prompts
if (prompts.isNullOrEmpty()) {
throw Exception("群聊中没有找到智能体,无法添加记忆")
}
// 使用第一个智能体的 openId
prompts.firstOrNull()?.openId
?: throw Exception("无法获取智能体信息")
}
if (openId.isBlank()) {
throw Exception("智能体ID不能为空")
}
// 创建智能体规则(群记忆)
val requestBody = CreatePromptRuleRequestBody(
rule = memoryText,
openId = openId
)
val response = ApiClient.api.createPromptRule(requestBody)
if (response.isSuccessful) {
addMemorySuccess = true
Log.d("GroupChatInfoViewModel", "群记忆添加成功")
// 刷新记忆列表和配额
loadMemoryQuota(openId)
loadMemoryList(openId)
} else {
val errorResponse = parseErrorResponse(response.errorBody())
val errorMessage = errorResponse?.toServiceException()?.message
?: "添加群记忆失败: ${response.code()}"
throw Exception(errorMessage)
}
} catch (e: Exception) {
addMemoryError = e.message ?: "添加群记忆失败"
Log.e("GroupChatInfoViewModel", "添加群记忆失败: ${e.message}", e)
} finally {
isAddingMemory = false
}
}
}
/**
* 获取记忆配额信息
*/
fun loadMemoryQuota(openId: String? = null) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
val targetOpenId = openId ?: promptOpenId
if (targetOpenId.isNullOrBlank()) {
// 如果还没有获取到 openId先获取
val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId)
val groupChatResponse = response.body()?.data
val prompts = groupChatResponse?.prompts
val fetchedOpenId = prompts?.firstOrNull()?.openId
?: throw Exception("无法获取智能体信息")
promptOpenId = fetchedOpenId
val quotaResponse = ApiClient.api.getPromptRuleQuota(fetchedOpenId)
if (quotaResponse.isSuccessful) {
memoryQuota = quotaResponse.body()?.data
} else {
throw Exception("获取配额信息失败: ${quotaResponse.code()}")
}
} else {
val quotaResponse = ApiClient.api.getPromptRuleQuota(targetOpenId)
if (quotaResponse.isSuccessful) {
memoryQuota = quotaResponse.body()?.data
} else {
throw Exception("获取配额信息失败: ${quotaResponse.code()}")
}
}
} catch (e: Exception) {
memoryError = e.message ?: "获取配额信息失败"
Log.e("GroupChatInfoViewModel", "获取配额信息失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
/**
* 获取记忆列表
*/
fun loadMemoryList(openId: String? = null, page: Int = 1, pageSize: Int = 20) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
val targetOpenId = openId ?: promptOpenId
if (targetOpenId.isNullOrBlank()) {
// 如果还没有获取到 openId先获取
val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId)
val groupChatResponse = response.body()?.data
val prompts = groupChatResponse?.prompts
val fetchedOpenId = prompts?.firstOrNull()?.openId
?: throw Exception("无法获取智能体信息")
promptOpenId = fetchedOpenId
val listResponse = ApiClient.api.getPromptRuleList(fetchedOpenId, page = page, pageSize = pageSize)
if (listResponse.isSuccessful) {
memoryList = listResponse.body()?.data?.list ?: emptyList()
} else {
throw Exception("获取记忆列表失败: ${listResponse.code()}")
}
} else {
val listResponse = ApiClient.api.getPromptRuleList(targetOpenId, page = page, pageSize = pageSize)
if (listResponse.isSuccessful) {
memoryList = listResponse.body()?.data?.list ?: emptyList()
} else {
throw Exception("获取记忆列表失败: ${listResponse.code()}")
}
}
} catch (e: Exception) {
memoryError = e.message ?: "获取记忆列表失败"
Log.e("GroupChatInfoViewModel", "获取记忆列表失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
/**
* 删除记忆
*/
fun deleteMemory(ruleId: Int) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
val response = ApiClient.api.deletePromptRule(ruleId)
if (response.isSuccessful) {
// 刷新记忆列表和配额
promptOpenId?.let { openId ->
loadMemoryQuota(openId)
loadMemoryList(openId)
}
} else {
val errorResponse = parseErrorResponse(response.errorBody())
val errorMessage = errorResponse?.toServiceException()?.message
?: "删除记忆失败: ${response.code()}"
throw Exception(errorMessage)
}
} catch (e: Exception) {
memoryError = e.message ?: "删除记忆失败"
Log.e("GroupChatInfoViewModel", "删除记忆失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
/**
* 更新记忆
*/
fun updateMemory(ruleId: Int, newRuleText: String, targetOpenId: String? = null) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
val openId = targetOpenId ?: promptOpenId
?: throw Exception("无法获取智能体ID")
val requestBody = com.aiosman.ravenow.data.api.UpdatePromptRuleRequestBody(
id = ruleId,
rule = newRuleText,
openId = openId
)
val response = ApiClient.api.updatePromptRule(requestBody)
if (response.isSuccessful) {
// 刷新记忆列表和配额
loadMemoryQuota(openId)
loadMemoryList(openId)
} else {
val errorResponse = parseErrorResponse(response.errorBody())
val errorMessage = errorResponse?.toServiceException()?.message
?: "更新记忆失败: ${response.code()}"
throw Exception(errorMessage)
}
} catch (e: Exception) {
memoryError = e.message ?: "更新记忆失败"
Log.e("GroupChatInfoViewModel", "更新记忆失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
}

View File

@@ -0,0 +1,625 @@
package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
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 androidx.compose.foundation.Image
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import android.widget.Toast
import androidx.compose.ui.graphics.Brush
@Composable
fun GroupMemoryManageContent(
groupId: String,
viewModel: GroupChatInfoViewModel,
onAddMemoryClick: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val sheetHeight = screenHeight * 0.95f
val context = LocalContext.current
// 编辑记忆的状态 - 存储正在编辑的记忆ID
var editingMemoryId by remember { mutableStateOf<Int?>(null) }
// 加载配额和列表数据
LaunchedEffect(Unit) {
viewModel.loadMemoryQuota()
viewModel.loadMemoryList()
}
val quota = viewModel.memoryQuota
val memoryList = viewModel.memoryList
val isLoading = viewModel.isLoadingMemory
Column(
modifier = Modifier
.fillMaxWidth()
.height(sheetHeight)
.background(Color(0xFFFAF9FB))
) {
// 顶部栏:返回按钮 + 标题 + 加号按钮
Box(
modifier = Modifier
.fillMaxWidth()
.height(44.dp)
.padding(horizontal = 16.dp)
) {
// 中间标题 - 绝对居中,不受其他组件影响
Text(
text = "记忆管理",
style = TextStyle(
color = Color.Black,
fontSize = 17.sp,
fontWeight = FontWeight.SemiBold
),
modifier = Modifier.align(Alignment.Center),
textAlign = TextAlign.Center
)
// 左侧返回按钮
Row(
modifier = Modifier
.align(Alignment.CenterStart)
.clip(RoundedCornerShape(296.dp))
.background(Color.White) // 浅灰色背景
.noRippleClickable { onDismiss() }
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Image(
painter = painterResource(R.drawable.rider_pro_back_icon),
contentDescription = "返回",
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(Color.Black)
)
Text(
text = "返回",
style = TextStyle(
color = Color.Black,
fontSize = 15.sp,
fontWeight = FontWeight.Normal
)
)
}
// 右侧圆形加号按钮
Box(
modifier = Modifier
.align(Alignment.CenterEnd)
.size(32.dp)
.clip(CircleShape)
.background(Color.White)
.noRippleClickable { onAddMemoryClick() },
contentAlignment = Alignment.Center
) {
Text(
text = "+",
style = TextStyle(
color = Color.Black,
fontSize = 20.sp,
fontWeight = FontWeight.Medium
)
)
}
}
// 浅黄色提示栏 - 显示真实的配额数据
Row(
modifier = Modifier
.fillMaxWidth()
.background(Color(0xFFFBF8EF))
.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("已付费:", style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp))
Spacer(Modifier.width(3.dp))
Text(
"${quota?.purchasedCount ?: 0}",
style = TextStyle(color = Color(0xFFFF8D28), fontSize = 13.sp)
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text("已使用:", style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp))
Spacer(Modifier.width(3.dp))
Text(
"${quota?.currentCount ?: 0}",
style = TextStyle(color = Color(0xFFFF8D28), fontSize = 13.sp)
)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text("可用上限:", style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp))
Spacer(Modifier.width(3.dp))
Text(
"50",
style = TextStyle(color = Color(0xFFFF8D28), fontSize = 13.sp)
)
}
}
// 记忆列表或空状态
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
if (isLoading) {
// 加载中状态
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(40.dp),
color = Color(0xFFFF8D28)
)
}
} else if (memoryList.isNotEmpty()) {
// 显示记忆列表
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(memoryList) { memory ->
MemoryItem(
memory = memory,
isEditing = editingMemoryId == memory.id,
onEdit = {
editingMemoryId = memory.id
},
onCancel = {
editingMemoryId = null
},
onSave = { newText ->
viewModel.updateMemory(memory.id, newText)
editingMemoryId = null
},
onDelete = {
viewModel.deleteMemory(memory.id)
}
)
}
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(id = R.mipmap.group),
contentDescription = "暂无记忆",
modifier = Modifier
.height(150.dp).width(180.dp)
)
Spacer(Modifier.height(10.dp))
Text(
text = "暂无记忆",
style = TextStyle(color = Color.Black, fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
)
Spacer(Modifier.height(6.dp))
Text(
text = "点击上方按钮添加群记忆",
style = TextStyle(color = Color.Black, fontSize = 14.sp, fontWeight = FontWeight.Normal)
)
}
}
}
}
}
/**
* 编辑记忆对话框
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditGroupMemoryDialog(
memory: com.aiosman.ravenow.data.api.PromptRule,
viewModel: GroupChatInfoViewModel,
onDismiss: () -> Unit,
onUpdateMemory: (String) -> Unit
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
var memoryText by remember { mutableStateOf(memory.rule) }
val maxLength = 500
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val gradientColors = listOf(
Color(0xFF7C45ED),
Color(0xFF7C57EE),
Color(0xFF7BD8F8)
)
val gradientBrush = Brush.horizontalGradient(colors = gradientColors)
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
containerColor = Color(0xFFFAF9FB),
dragHandle = {
Box(
modifier = Modifier
.width(36.dp)
.height(5.dp)
.padding(top = 5.dp)
.background(
Color(0xFFCCCCCC),
RoundedCornerShape(100.dp)
)
)
},
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(88.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(16.dp))
.background(brush = gradientBrush)
)
Box(
modifier = Modifier
.fillMaxSize()
.padding(1.dp)
.clip(RoundedCornerShape(15.dp))
.background(Color.White)
.padding(12.dp),
contentAlignment = Alignment.TopStart
) {
BasicTextField(
value = memoryText,
onValueChange = { newText ->
if (newText.length <= maxLength) {
memoryText = newText
}
},
cursorBrush = SolidColor(Color.Black),
modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle(
fontSize = 13.sp,
color = Color.Black,
lineHeight = 18.sp
),
decorationBox = { innerTextField ->
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.TopStart
) {
innerTextField()
}
}
)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// 取消按钮
Box(
modifier = Modifier
.weight(1f)
.height(34.dp)
.clip(RoundedCornerShape(653.8.dp))
.background(Color(0x147C7480))
.noRippleClickable { onDismiss() },
contentAlignment = Alignment.Center
) {
Text(
text = "取消",
style = TextStyle(
fontSize = 15.sp,
color = Color.Black
)
)
}
// 保存按钮
Box(
modifier = Modifier
.weight(1f)
.height(34.dp)
.clip(RoundedCornerShape(653.8.dp))
.background(Color(0xFF110C13))
.noRippleClickable {
if (memoryText.isNotBlank() && memoryText != memory.rule) {
onUpdateMemory(memoryText)
Toast.makeText(context, "记忆更新成功", Toast.LENGTH_SHORT).show()
}
},
contentAlignment = Alignment.Center
) {
Text(
text = "保存",
style = TextStyle(
fontSize = 15.sp,
color = Color.White
)
)
}
}
}
}
}
/**
* 记忆项组件
*/
@Composable
fun MemoryItem(
memory: com.aiosman.ravenow.data.api.PromptRule,
isEditing: Boolean = false,
onEdit: () -> Unit = {},
onCancel: () -> Unit = {},
onSave: (String) -> Unit = {},
onDelete: () -> Unit
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
var memoryText by remember { mutableStateOf(memory.rule) }
val maxLength = 500
// 渐变边框颜色
val gradientColors = listOf(
Color(0xFF7C45ED),
Color(0xFF7C57EE),
Color(0xFF7BD8F8)
)
val gradientBrush = Brush.horizontalGradient(colors = gradientColors)
// 当进入编辑模式时,重置文本
LaunchedEffect(isEditing) {
if (isEditing) {
memoryText = memory.rule
}
}
if (isEditing) {
// 编辑模式:显示编辑界面
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(Color.White)
.padding(horizontal = 12.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// 文本输入框 - 带渐变边框
Box(
modifier = Modifier
.fillMaxWidth()
.height(88.dp)
) {
// 渐变边框层
Box(
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(16.dp))
.background(brush = gradientBrush)
)
// 内容层 - 白色背景通过padding形成边框效果
Box(
modifier = Modifier
.fillMaxSize()
.padding(1.dp)
.clip(RoundedCornerShape(15.dp))
.background(Color.White)
.padding(12.dp),
contentAlignment = Alignment.TopStart
) {
BasicTextField(
value = memoryText,
onValueChange = { newText ->
if (newText.length <= maxLength) {
memoryText = newText
}
},
cursorBrush = SolidColor(Color.Black),
modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle(
fontSize = 13.sp,
color = Color.Black,
lineHeight = 18.sp
),
decorationBox = { innerTextField ->
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.TopStart
) {
innerTextField()
}
}
)
}
}
// 按钮行:取消和保存
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// 取消按钮
Box(
modifier = Modifier
.weight(1f)
.height(34.dp)
.clip(RoundedCornerShape(653.8.dp))
.background(Color(0x147C7480))
.noRippleClickable { onCancel() },
contentAlignment = Alignment.Center
) {
Text(
text = "取消",
style = TextStyle(
fontSize = 15.sp,
color = Color.Black
)
)
}
// 保存按钮
Box(
modifier = Modifier
.weight(1f)
.height(34.dp)
.clip(RoundedCornerShape(653.8.dp))
.background(Color(0xFF110C13))
.noRippleClickable {
if (memoryText.isNotBlank() && memoryText != memory.rule) {
onSave(memoryText)
Toast.makeText(context, "记忆更新成功", Toast.LENGTH_SHORT).show()
}
},
contentAlignment = Alignment.Center
) {
Text(
text = "保存",
style = TextStyle(
fontSize = 15.sp,
color = Color.White
)
)
}
}
}
} else {
// 显示模式:显示记忆内容
// 格式化日期:从 "2025-10-20T10:30:00Z" 格式转换为 "2025年10月20日"
val formattedDate = try {
if (memory.createdAt.length >= 10) {
val dateStr = memory.createdAt.substring(0, 10)
val parts = dateStr.split("-")
if (parts.size == 3) {
"${parts[0]}${parts[1].toInt()}${parts[2].toInt()}"
} else {
dateStr
}
} else {
memory.createdAt
}
} catch (e: Exception) {
if (memory.createdAt.length >= 10) memory.createdAt.substring(0, 10) else memory.createdAt
}
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(Color.White)
.padding(horizontal = 12.dp, vertical = 16.dp)
) {
// 主文本 - 顶部
Text(
text = memory.rule,
style = TextStyle(
color = Color.Black,
fontSize = 13.sp,
lineHeight = 18.sp
),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
// 底部行:日期 + 编辑删除按钮
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom
) {
// 日期文本 - 左侧
Text(
text = formattedDate,
style = TextStyle(
color = Color(0x993C3C43),
fontSize = 11.sp
)
)
// 编辑和删除图标 - 右侧
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 编辑图标
Image(
painter = painterResource(R.mipmap.icons_infor_edit),
contentDescription = "编辑",
modifier = Modifier
.size(20.dp)
.noRippleClickable { onEdit() },
colorFilter = ColorFilter.tint(Color.Black)
)
Image(
painter = painterResource(R.mipmap.iconsdelete),
contentDescription = "删除",
modifier = Modifier
.size(20.dp)
.noRippleClickable { onDelete() },
colorFilter = ColorFilter.tint(Color(0xFFEE2A33))
)
}
}
}
}
}

View File

@@ -95,21 +95,21 @@ fun AgentChatListScreen() {
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_znt_qs_as_img
else R.mipmap.invalid_name_5),
contentDescription = "null data",
modifier = Modifier
.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.agent_chat_empty_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.juhao_dark
else R.mipmap.invalid_name_5),
contentDescription = "null data",
modifier = Modifier
.size(width = 181.dp, height = 153.dp)
)
Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
Text(
text = stringResource(R.string.agent_chat_empty_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.agent_chat_empty_subtitle),

View File

@@ -175,13 +175,13 @@ fun AllChatListScreen() {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_py_qs_as_img
id = if(AppState.darkMode) R.mipmap.piao_dark
else R.mipmap.invalid_name_2),
contentDescription = "null data",
modifier = Modifier
.size(181.dp)
.size(width = 181.dp, height = 153.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
Text(
text = stringResource(R.string.friend_chat_empty_title),
color = AppColors.text,

View File

@@ -83,21 +83,21 @@ fun FriendChatListScreen() {
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_py_qs_as_img
else R.mipmap.invalid_name_2),
contentDescription = "null data",
modifier = Modifier
.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_empty_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.piao_dark
else R.mipmap.invalid_name_2),
contentDescription = "null data",
modifier = Modifier
.size(width = 181.dp, height = 153.dp)
)
Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
Text(
text = stringResource(R.string.friend_chat_empty_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_empty_subtitle),

View File

@@ -75,21 +75,21 @@ fun GroupChatListScreen() {
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_ql_qs_as_img
else R.mipmap.invalid_name_12),
contentDescription = "null data",
modifier = Modifier
.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.group_chat_empty),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.fei_dark
else R.mipmap.invalid_name_12),
contentDescription = "null data",
modifier = Modifier
.size(width = 181.dp, height = 153.dp)
)
Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
Text(
text = stringResource(R.string.group_chat_empty),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.group_chat_empty_join),

View File

@@ -118,7 +118,7 @@ fun ProfileV3(
onLike: (MomentEntity) -> Unit = {},
onComment: (MomentEntity) -> Unit = {},
onAgentClick: (AgentEntity) -> Unit = {},
postCount: Int? = null, // 新增参数用于传递帖子总数
postCount: Long? = null, // 新增参数用于传递帖子总数
) {
val model = MyProfileViewModel
val pagerState = rememberPagerState(pageCount = { if (isAiAccount) 1 else 2 })
@@ -357,7 +357,7 @@ fun ProfileV3(
profile?.let {
UserItem(
accountProfileEntity = it,
postCount = postCount ?: if (isSelf) MyProfileViewModel.momentLoader.total else moments.size
postCount = postCount ?: if (isSelf) MyProfileViewModel.momentLoader.total else moments.size.toLong()
)
}
}

View File

@@ -189,16 +189,17 @@ fun GalleryGrid(
) {
Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_dt_qs_as_img
id = if(AppState.darkMode) R.mipmap.shuihu_dark
else R.mipmap.invalid_name_7),
contentDescription = "暂无图片",
modifier = Modifier.size(181.dp),
modifier = Modifier
.size(width = 181.dp, height = 153.dp),
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(if(AppState.darkMode) 9.dp else 24.dp))
Text(
text = "故事还没开始",
text = "你的故事还没开始",
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600

View File

@@ -212,7 +212,7 @@ fun EmptyAgentsView() {
if (isNetworkAvailable) {
Image(
painter = painterResource(
id =if(AppState.darkMode) R.mipmap.ai_dark
id = if(AppState.darkMode) R.mipmap.ai_dark
else R.mipmap.ai),
contentDescription = "暂无Agent",
modifier = Modifier
@@ -220,7 +220,8 @@ fun EmptyAgentsView() {
.align(Alignment.CenterHorizontally),
)
Spacer(modifier = Modifier.height(24.dp))
// 根据是否为深色模式调整间距
Spacer(modifier = Modifier.height(if(AppState.darkMode) 9.dp else 24.dp))
Text(
text = "专属AI等你召唤",

View File

@@ -33,7 +33,7 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable
fun UserItem(
accountProfileEntity: AccountProfileEntity,
postCount: Int = 0
postCount: Long = 0
) {
val navController = LocalNavController.current
val AppColors = LocalAppTheme.current

View File

@@ -123,14 +123,15 @@ fun LikeNoticeScreen() {
) {
Image(
painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_z_qs_as_img
id = if(AppState.darkMode) R.mipmap.sanqiu_dark
else R.mipmap.invalid_name_6),
contentDescription = "No Notice",
modifier = Modifier.size(181.dp)
modifier = Modifier
.size(width = 181.dp, height = 153.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
Text(
text = "你的赞在路上",
text = "你的赞在赶来的路上",
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,

View File

@@ -26,7 +26,7 @@ fun SplashScreen() {
) {
// 居中的图标
Image(
painter = painterResource(id = R.mipmap.invalid_name),
painter = painterResource(id = R.mipmap.kp_logo_img),
contentDescription = "App Logo",
modifier = Modifier
.align(Alignment.Center)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -230,7 +230,7 @@
<string name="tab_recommend">おすすめ</string>
<string name="tab_short_video">ショート動画</string>
<string name="tab_news">ニュース</string>
<!-- Block Confirm Dialog -->
<string name="confirm_block_user">%1$sをブロックしますか</string>
<string name="block_description_1">相手はあなたにメッセージを送信したり、あなたのプロフィールやコンテンツを見つけることができなくなります</string>
@@ -248,5 +248,38 @@
<string name="select_apply_to_use_theme">「適用」を選択してこのテーマを使用</string>
<string name="tap_cancel_to_preview_other_themes">「キャンセル」をタップして他のテーマをプレビュー</string>
<!-- Group Chat Info -->
<string name="group_chat_info_title">グループチャット情報</string>
<string name="group_chat_info_add_member">メンバーを追加</string>
<string name="group_chat_info_notification">通知</string>
<string name="group_chat_info_share">共有</string>
<string name="group_chat_info_unlock_extension">グループ拡張機能を解除</string>
<string name="group_chat_info_group_memory">グループメモリ</string>
<string name="group_chat_info_memory_description">AIは記憶に基づいてグループチャットであなたをより理解します</string>
<string name="group_chat_info_add_memory">メモリを追加</string>
<string name="group_chat_info_memory_manage">メモリ管理</string>
<string name="group_chat_info_group_settings">グループ設定</string>
<string name="group_chat_info_group_visibility">グループの可視性</string>
<string name="group_chat_info_locked">ロック中</string>
<string name="group_chat_info_member_manage">メンバー管理</string>
<string name="group_chat_info_wallpaper">グループチャット壁紙</string>
<string name="group_chat_info_dissolve">グループチャットを解散</string>
<string name="group_chat_info_add_group_memory">グループメモリを追加</string>
<string name="group_chat_info_member_count">%d人のメンバー</string>
<string name="group_chat_info_memory_input_hint">グループの記憶内容を入力、例えば、グループのメンバーは科学技術と設計を議論するのが好きである....</string>
<string name="group_chat_info_memory_cost">メモリを追加すると20コインを消費します</string>
<string name="group_chat_info_memory_optimization">AIは記憶に基づいて返信を最適化します</string>
<string name="group_chat_info_memory_editable">いつでも編集または削除できます</string>
<string name="group_chat_info_memory_add_success">グループメモリが正常に追加されました</string>
<string name="group_chat_info_permission_settings">グループ権限設定</string>
<string name="group_chat_info_public_group">公開グループ</string>
<string name="group_chat_info_public_group_desc">誰でも検索して参加できます</string>
<string name="group_chat_info_private_group">プライベートグループ</string>
<string name="group_chat_info_private_group_desc">招待のみ</string>
<string name="group_chat_info_private_group_cost">50コイン</string>
<string name="group_chat_info_balance">残高: %1$dコイン</string>
<string name="group_chat_info_unlock_cost">アンロック費用: %1$dコイン</string>
<string name="group_chat_info_done">完了</string>
<string name="group_chat_info_recharge_hint">チャージしてより多くのコインを獲得できます</string>
</resources>

View File

@@ -152,8 +152,8 @@
<string name="favourites_null">暂无数据</string>
<string name="agent_chat_list_title">智能体聊天</string>
<string name="agent_chat_empty_title">AI 在等你的开场白</string>
<string name="agent_chat_empty_subtitle">去首页探索一下,主动发起对话!</string>
<string name="agent_chat_empty_title">AI在等你开启第一句对话</string>
<string name="agent_chat_empty_subtitle">去首页探索一下,主动发起一场对话!</string>
<string name="agent_chat_me_prefix">我: </string>
<string name="agent_chat_image">[图片]</string>
<string name="agent_chat_voice">[语音]</string>
@@ -163,12 +163,12 @@
<string name="agent_chat_load_failed">加载失败</string>
<string name="agent_chat_load_more_failed">加载更多失败</string>
<string name="agent_chat_user_info_failed">获取用户信息失败: %s</string>
<string name="group_chat_empty">没有群聊宇宙安静</string>
<string name="group_chat_empty">没有群聊消息的宇宙安静</string>
<string name="group_chat_empty_title">没有群聊消息的宇宙太安静了</string>
<string name="group_chat_empty_subtitle">在首页探索感兴趣的主题房间</string>
<string name="group_chat_empty_join">去首页探索感兴趣的高能对话</string>
<string name="friend_chat_empty_title">和朋友,还没有对话哦~</string>
<string name="friend_chat_empty_subtitle">点击好友头像,即刻发起聊天</string>
<string name="group_chat_empty_join">去首页探索感兴趣的主题房间</string>
<string name="friend_chat_empty_title">和朋友,还没说第一句话呢</string>
<string name="friend_chat_empty_subtitle">一段崭新的友谊 等待被唤醒</string>
<string name="friend_chat_me_prefix">我: </string>
<string name="friend_chat_load_failed">加载失败</string>
<string name="create_group_chat">创建群聊</string>
@@ -225,15 +225,15 @@
<string name="friend_chat_no_network_title">掉线啦...</string>
<string name="friend_chat_no_network_subtitle">确认一下网络,连接这个宇宙</string>
<string name="Reload">重新加载</string>
<!-- Login page -->
<string name="join_party_carnival">加入派派,一起狂欢</string>
<!-- Tab labels -->
<string name="tab_recommend">推荐</string>
<string name="tab_short_video">短视频</string>
<string name="tab_news">新闻</string>
<!-- Block Confirm Dialog -->
<string name="confirm_block_user">确认拉黑%s</string>
<string name="block_description_1">对方将无法发消息给你,也无法找到你的主页或内容</string>
@@ -250,4 +250,45 @@
<string name="each_theme_unique_experience">每个主题都有自己独特的体验</string>
<string name="select_apply_to_use_theme">选择"应用"可选中这个主题</string>
<string name="tap_cancel_to_preview_other_themes">轻触"取消"可预览其他主题</string>
<!-- Group Chat Info -->
<string name="group_chat_info_title">群聊信息</string>
<string name="group_chat_info_add_member">添加成员</string>
<string name="group_chat_info_notification">通知</string>
<string name="group_chat_info_share">分享</string>
<string name="group_chat_info_unlock_extension">解锁群扩展</string>
<string name="group_chat_info_group_memory">群记忆</string>
<string name="group_chat_info_memory_description">AI 会根据记忆在群聊里更懂你</string>
<string name="group_chat_info_add_memory">添加记忆</string>
<string name="group_chat_info_memory_manage">记忆管理</string>
<string name="group_chat_info_group_settings">群资料设置</string>
<string name="group_chat_info_group_visibility">群可见性</string>
<string name="group_chat_info_locked">待解锁</string>
<string name="group_chat_info_member_manage">成员管理</string>
<string name="group_chat_info_wallpaper">群聊壁纸</string>
<string name="group_chat_info_dissolve">解散群聊</string>
<string name="group_chat_info_add_group_memory">添加群记忆</string>
<string name="group_chat_info_member_count">%d 位成员</string>
<string name="group_chat_info_memory_input_hint">输入群记忆内容,例如:群成员喜欢讨论科技和设计....</string>
<string name="group_chat_info_memory_cost">添加记忆需消耗 20 派币</string>
<string name="group_chat_info_memory_optimization">AI 将基于记忆优化回复</string>
<string name="group_chat_info_memory_editable">可随时编辑或删除</string>
<string name="group_chat_info_memory_add_success">群记忆添加成功</string>
<string name="group_chat_info_permission_settings">群权限设置</string>
<string name="group_chat_info_public_group">公开群组</string>
<string name="group_chat_info_public_group_desc">任何人都可搜索并加入</string>
<string name="group_chat_info_private_group">私密群组</string>
<string name="group_chat_info_private_group_desc">仅限邀请加入</string>
<string name="group_chat_info_private_group_cost">50 派币</string>
<string name="group_chat_info_balance">余额: %1$d 派币</string>
<string name="group_chat_info_unlock_cost">解锁费用: %1$d 派币</string>
<string name="group_chat_info_done">完成</string>
<string name="group_chat_info_recharge_hint">可通过充值获得更多派币</string>
<!-- Edit Profile Extras -->
<string name="mbti_type">MBTI 类型</string>
<string name="zodiac">星座</string>
<string name="save">保存</string>
<string name="choose_mbti">选择 MBTI</string>
<string name="choose_zodiac">选择星座</string>
</resources>

View File

@@ -220,15 +220,15 @@
<string name="friend_chat_no_network_title">Offline…</string>
<string name="friend_chat_no_network_subtitle">Check your network to connect to this universe</string>
<string name="Reload">Reload</string>
<!-- Login page -->
<string name="join_party_carnival">Join the party, let\'s celebrate together</string>
<!-- Tab labels -->
<string name="tab_recommend">Recommend</string>
<string name="tab_short_video">Short Video</string>
<string name="tab_news">News</string>
<!-- Block Confirm Dialog -->
<string name="confirm_block_user">Confirm block %1$s?</string>
<string name="block_description_1">They won\'t be able to send you messages or find your profile or content</string>
@@ -246,5 +246,43 @@
<string name="select_apply_to_use_theme">Select "Apply" to use this theme</string>
<string name="tap_cancel_to_preview_other_themes">Tap "Cancel" to preview other themes</string>
<!-- Group Chat Info -->
<string name="group_chat_info_title">Group Chat Info</string>
<string name="group_chat_info_add_member">Add Member</string>
<string name="group_chat_info_notification">Notification</string>
<string name="group_chat_info_share">Share</string>
<string name="group_chat_info_unlock_extension">Unlock Group Extension</string>
<string name="group_chat_info_group_memory">Group Memory</string>
<string name="group_chat_info_memory_description">AI will understand you better in group chat based on memory</string>
<string name="group_chat_info_add_memory">Add Memory</string>
<string name="group_chat_info_memory_manage">Memory Management</string>
<string name="group_chat_info_group_settings">Group Settings</string>
<string name="group_chat_info_group_visibility">Group Visibility</string>
<string name="group_chat_info_locked">Locked</string>
<string name="group_chat_info_member_manage">Member Management</string>
<string name="group_chat_info_wallpaper">Group Chat Wallpaper</string>
<string name="group_chat_info_dissolve">Dissolve Group Chat</string>
<string name="group_chat_info_add_group_memory">Add Group Memory</string>
<string name="group_chat_info_member_count">%d members</string>
<string name="group_chat_info_memory_input_hint">Input group memory content, for example: Group members enjoy discussing technology and design …</string>
<string name="group_chat_info_memory_cost">Adding memory consumes 20 coins</string>
<string name="group_chat_info_memory_optimization">AI will optimize replies based on memory</string>
<string name="group_chat_info_memory_editable">Can be edited or deleted at any time</string>
<string name="group_chat_info_memory_add_success">Group memory added successfully</string>
<string name="group_chat_info_permission_settings">Group Permission Settings</string>
<string name="group_chat_info_public_group">Public Group</string>
<string name="group_chat_info_public_group_desc">Anyone can search and join</string>
<string name="group_chat_info_private_group">Private Group</string>
<string name="group_chat_info_private_group_desc">Invitation only</string>
<string name="group_chat_info_private_group_cost">50 coins</string>
<string name="group_chat_info_balance">Balance: %1$d coins</string>
<string name="group_chat_info_unlock_cost">Unlock cost: %1$d coins</string>
<string name="group_chat_info_done">Done</string>
<string name="group_chat_info_recharge_hint">You can recharge to get more coins</string>
<!-- Edit Profile Extras -->
<string name="mbti_type">MBTI</string>
<string name="zodiac">Zodiac</string>
<string name="save">Save</string>
<string name="choose_mbti">Choose MBTI</string>
<string name="choose_zodiac">Choose Zodiac</string>
</resources>