feat(notify): 站内通知补齐完成、到期逾期与知识事件
CI / backend (push) Waiting to run

This commit is contained in:
Steven
2026-11-10 13:47:56 +08:00
parent 4b7627d70e
commit f54df70811
15 changed files with 791 additions and 32 deletions
@@ -36,6 +36,7 @@ import com.dadao.task.ui.TicketsScreen
import com.dadao.task.ui.UpdateDialog
import com.dadao.task.ui.WorkspaceScaffold
import com.dadao.task.ui.WorkspaceTab
import kotlinx.coroutines.delay
class MainActivity : ComponentActivity() {
@@ -114,10 +115,14 @@ private fun AppRoot(joinUri: String?) {
.onFailure { pendingJoin = null }
}
// 未读数:进工作区、切页面、或通知页标了已读(unreadRefresh)时刷新
// 未读数:进工作区、切页面、或通知页标了已读(unreadRefresh)时刷新;
// 停留不动时每 30 秒再拉一次,这样"任务被指派/快到期/已逾期"不用切页面也能看到角标。
LaunchedEffect(current?.id, tab, unreadRefresh) {
val ws = current ?: return@LaunchedEffect
unread = api.unreadCount(ws.id)
while (true) {
unread = api.unreadCount(ws.id)
delay(30_000)
}
}
// 版本检查:登录成功后查一次就够,查不到(网络、没发布过包)就当没有新版
@@ -234,11 +239,15 @@ private fun AppRoot(joinUri: String?) {
WorkspaceTab.Notifications -> NotificationsScreen(
api = api,
workspaceId = ws.id,
onOpenItem = { itemId, isTicket ->
// 通知点开后跳到对应列表并展开那一条;通知本身不带数据,页面自己回源
onOpenItem = { itemId, itemKind ->
// 通知点开后跳到对应页签并展开那一条;通知本身不带数据,页面自己回源
reopenItem = itemId
reopenKnowledge = false
tab = if (isTicket) WorkspaceTab.Tickets else WorkspaceTab.Tasks
reopenKnowledge = itemKind == "knowledge"
tab = when (itemKind) {
"knowledge" -> WorkspaceTab.Knowledge
"ticket" -> WorkspaceTab.Tickets
else -> WorkspaceTab.Tasks
}
},
onUnreadChanged = { unreadRefresh += 1 },
)
@@ -328,6 +328,8 @@ data class AppNotification(
val id: String,
val kind: String = "",
@SerialName("item_id") val itemId: String? = null,
/** task / ticket / knowledge:决定点开跳到哪个页签。 */
@SerialName("item_kind") val itemKind: String? = null,
@SerialName("item_title") val itemTitle: String? = null,
val title: String = "",
val body: String = "",
@@ -32,6 +32,7 @@ import androidx.compose.ui.unit.sp
import com.dadao.task.data.Api
import com.dadao.task.data.ApiException
import com.dadao.task.data.AppNotification
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
@@ -44,7 +45,7 @@ import kotlinx.coroutines.launch
fun NotificationsScreen(
api: Api,
workspaceId: String,
onOpenItem: (itemId: String, isTicket: Boolean) -> Unit,
onOpenItem: (itemId: String, itemKind: String) -> Unit,
onUnreadChanged: () -> Unit,
refreshKey: Int = 0,
) {
@@ -54,17 +55,25 @@ fun NotificationsScreen(
var error by remember { mutableStateOf<String?>(null) }
val scope = rememberCoroutineScope()
suspend fun reload() {
loading = true
suspend fun reload(silent: Boolean = false) {
if (!silent) loading = true
error = null
runCatching { api.notifications(workspaceId, unreadOnly) }
.onSuccess { rows = it.notifications }
.onFailure { error = verifyStatusCode((it as? ApiException)?.code ?: 0) }
loading = false
if (!silent) loading = false
}
LaunchedEffect(workspaceId, unreadOnly, refreshKey) { reload() }
// 停在通知页时也要能看到新动静:每 30 秒静默拉一次,不闪加载态。
LaunchedEffect(workspaceId, unreadOnly) {
while (true) {
delay(30_000)
reload(silent = true)
}
}
Column(Modifier.fillMaxSize().padding(horizontal = 16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -85,6 +94,26 @@ fun NotificationsScreen(
bg = if (unreadOnly) PaperSunken else PrimarySoft,
modifier = Modifier.clickable { unreadOnly = false },
)
Box(Modifier.weight(1f))
SoftPill(
text = "刷新",
fg = InkMuted,
bg = PaperSunken,
modifier = Modifier.clickable { scope.launch { reload() } },
)
SoftPill(
text = "全部已读",
fg = InkMuted,
bg = PaperSunken,
modifier = Modifier.clickable {
scope.launch {
runCatching { api.markAllNotificationsRead(workspaceId) }
rows = rows.map { if (it.readAt == null) it.copy(readAt = "read") else it }
onUnreadChanged()
reload(silent = true)
}
},
)
}
Text(
@@ -121,7 +150,14 @@ fun NotificationsScreen(
contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = 20.dp),
) {
items(rows, key = { it.id }) { n ->
val isTicket = n.kind.startsWith("ticket_")
// 知识/工单/任务三种落点:优先看后端给的 item_kind,老数据再退回 kind 前缀
val isKnowledge = n.kind.startsWith("knowledge_") || n.itemKind == "knowledge"
val isTicket = !isKnowledge && (n.kind.startsWith("ticket_") || n.itemKind == "ticket")
val targetKind = when {
isKnowledge -> "knowledge"
isTicket -> "ticket"
else -> "task"
}
SoftCard(
Modifier.fillMaxWidth(),
onClick = {
@@ -131,7 +167,7 @@ fun NotificationsScreen(
rows = rows.map { if (it.id == n.id) it.copy(readAt = "read") else it }
onUnreadChanged()
}
n.itemId?.let { onOpenItem(it, isTicket) }
n.itemId?.let { onOpenItem(it, targetKind) }
}
},
) {
@@ -143,7 +179,11 @@ fun NotificationsScreen(
contentAlignment = Alignment.Center,
) {
Text(
text = if (isTicket) "工" else "记",
text = when {
isKnowledge -> "知"
isTicket -> "工"
else -> "记"
},
fontSize = 11.sp,
fontWeight = FontWeight.SemiBold,
color = if (n.readAt == null) Primary else InkMuted,