之前安卓代码一行都没编译过,这次从零把编译链搭起来,第一遍编译就炸了 5 处。 代码 bug(5 个): - Api.kt 的 SmsRequest/LoginRequest/WorkspaceCreateRequest/RefreshRequest 都没加 @Serializable,json.encodeToString(x) 于是匹配到 encodeToString(strategy, value) 重载, 报 "SerializationStrategy<T> expected" + "No value passed for parameter 'value'" - 三处 mapOf/buildMap 拼请求体(Map<String, Any>)没法序列化,全换成具名 @Serializable 请求类 - ticketAction 的 body 参数从 Map<String, Any> 改成 String,避免把序列化责任散在调用方 - KnowledgeScreen 的 Card(Modifier, onClick=…) 命中无点击的重载,参数重名报错, 改成 Card(onClick=…, modifier=…) - 补 import kotlinx.serialization.encodeToString 环境固化(写进仓库,附理由注释,避免下次重踩): - gradle.properties 加 kotlin.compiler.execution.strategy=in-process (受限环境里 Kotlin 编译守护进程连不回来) - settings.gradle.kts 先走阿里云镜像再回落 google()/mavenCentral(),依赖下载从 5 分钟 2MB 变 90 秒 94MB - 工程内 debug keystore(android/keystore/debug.keystore)+ app/build.gradle.kts 显式指向 (受限用户对 ~/.android 无写权限,用系统默认 debug.keystore 会 AccessDenied) - app/src/debug/AndroidManifest.xml 给 debug 变体放开明文 HTTP: 真机连 http://192.168.8.x:8100 需要它,release 仍受 network_security_config 白名单约束 - android/README.md 换成实测过的构建/安装说明 + 坑表 结果:BUILD SUCCESSFUL,app-debug.apk 9.94 MB。 未验证:本机没有 system-image、也没连设备,所以只到"能编译、能装",没跑过界面。
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
debug 变体专用清单。
|
||||
|
||||
开发时后端跑在局域网明文 HTTP 上(http://192.168.8.x:8100),系统默认禁止明文流量,
|
||||
真机装上会直接连不上。这里只在 debug 放开明文,release 仍走 network_security_config 的白名单。
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:replace="android:usesCleartextTraffic" />
|
||||
</manifest>
|
||||
@@ -18,6 +18,7 @@ import com.dadao.task.data.TokenStore
|
||||
import com.dadao.task.data.Workspace
|
||||
import com.dadao.task.ui.DadaoTheme
|
||||
import com.dadao.task.ui.HomeScreen
|
||||
import com.dadao.task.ui.KnowledgeScreen
|
||||
import com.dadao.task.ui.LoginScreen
|
||||
import com.dadao.task.ui.TaskListScreen
|
||||
import com.dadao.task.ui.TicketsScreen
|
||||
@@ -36,7 +37,7 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Screen { Splash, Login, Home, Tasks, Tickets }
|
||||
private enum class Screen { Splash, Login, Home, Tasks, Tickets, Knowledge }
|
||||
|
||||
@Composable
|
||||
private fun AppRoot() {
|
||||
@@ -83,6 +84,7 @@ private fun AppRoot() {
|
||||
workspace = ws,
|
||||
onBack = { screen = Screen.Home },
|
||||
onOpenTickets = { screen = Screen.Tickets },
|
||||
onOpenKnowledge = { screen = Screen.Knowledge },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -94,5 +96,13 @@ private fun AppRoot() {
|
||||
TicketsScreen(api = api, workspace = ws, onBack = { screen = Screen.Tasks })
|
||||
}
|
||||
}
|
||||
Screen.Knowledge -> {
|
||||
val ws = current
|
||||
if (ws == null) {
|
||||
screen = Screen.Home
|
||||
} else {
|
||||
KnowledgeScreen(api = api, workspace = ws, onBack = { screen = Screen.Tasks })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
@@ -162,11 +163,102 @@ data class TicketPage(
|
||||
@SerialName("has_more") val hasMore: Boolean = false,
|
||||
)
|
||||
|
||||
/** 知识的附带信息,与 backend/app/api/knowledge.py 的 `_doc_out` 对齐。 */
|
||||
@Serializable
|
||||
data class KnowledgeInfo(
|
||||
@SerialName("doc_type") val docType: String = "note",
|
||||
val summary: String = "",
|
||||
@SerialName("status_hint") val statusHint: String = "",
|
||||
@SerialName("source_kind") val sourceKind: String = "manual",
|
||||
@SerialName("source_item_id") val sourceItemId: String? = null,
|
||||
@SerialName("generated_by") val generatedBy: String = "human",
|
||||
@SerialName("published_at") val publishedAt: String? = null,
|
||||
)
|
||||
|
||||
/** 知识的来源(工单或任务),可能已不存在——上游删了就不给。 */
|
||||
@Serializable
|
||||
data class SourceRef(
|
||||
val id: String = "",
|
||||
val kind: String = "",
|
||||
val title: String = "",
|
||||
val status: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KnowledgeDoc(
|
||||
val id: String,
|
||||
val title: String,
|
||||
@SerialName("body_md") val bodyMd: String = "",
|
||||
val status: String = "draft",
|
||||
val version: Int = 1,
|
||||
val tags: List<Tag> = emptyList(),
|
||||
val knowledge: KnowledgeInfo = KnowledgeInfo(),
|
||||
val source: SourceRef? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KnowledgePage(
|
||||
val items: List<KnowledgeDoc> = emptyList(),
|
||||
@SerialName("next_cursor") val nextCursor: String? = null,
|
||||
@SerialName("has_more") val hasMore: Boolean = false,
|
||||
)
|
||||
|
||||
// ---- 请求体。每个都要 @Serializable,否则 encodeToString 找不到序列化器 ----
|
||||
|
||||
@Serializable
|
||||
private data class SmsRequest(val phone: String)
|
||||
|
||||
@Serializable
|
||||
private data class LoginRequest(val phone: String, val code: String)
|
||||
|
||||
@Serializable
|
||||
private data class RefreshRequest(@SerialName("refresh_token") val refreshToken: String)
|
||||
|
||||
@Serializable
|
||||
private data class WorkspaceCreateRequest(val name: String)
|
||||
|
||||
@Serializable
|
||||
private data class JoinRequest(val code: String)
|
||||
|
||||
@Serializable
|
||||
private data class CreateItemRequest(val title: String, val status: String)
|
||||
|
||||
@Serializable
|
||||
private data class StatusRequest(val status: String, val version: Int)
|
||||
|
||||
@Serializable
|
||||
private data class CommentRequest(@SerialName("body_md") val bodyMd: String)
|
||||
|
||||
@Serializable
|
||||
private data class PromoteRequest(
|
||||
@SerialName("assignee_id") val assigneeId: String,
|
||||
val version: Int? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class VerifyRequest(
|
||||
val approve: Boolean,
|
||||
val note: String,
|
||||
@SerialName("resolution_md") val resolution: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class KnowledgeCreateRequest(
|
||||
val title: String,
|
||||
@SerialName("body_md") val bodyMd: String,
|
||||
@SerialName("doc_type") val docType: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class KnowledgeActionRequest(val action: String)
|
||||
|
||||
@Serializable
|
||||
private data class KnowledgeUpdateRequest(
|
||||
val version: Int,
|
||||
val title: String? = null,
|
||||
@SerialName("body_md") val bodyMd: String? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* 后端 API 客户端。
|
||||
*
|
||||
@@ -217,7 +309,7 @@ class Api(private val tokens: TokenStore) {
|
||||
|
||||
suspend fun join(workspaceId: String, code: String): Workspace = call(
|
||||
path = "/workspaces/$workspaceId/join",
|
||||
body = json.encodeToString(mapOf("code" to code)),
|
||||
body = json.encodeToString(JoinRequest(code)),
|
||||
auth = true,
|
||||
)
|
||||
|
||||
@@ -231,7 +323,7 @@ class Api(private val tokens: TokenStore) {
|
||||
|
||||
suspend fun createItem(workspaceId: String, title: String, status: String = "inbox"): Item = call(
|
||||
path = "/workspaces/$workspaceId/items",
|
||||
body = json.encodeToString(mapOf("title" to title, "status" to status)),
|
||||
body = json.encodeToString(CreateItemRequest(title, status)),
|
||||
auth = true,
|
||||
)
|
||||
|
||||
@@ -239,7 +331,7 @@ class Api(private val tokens: TokenStore) {
|
||||
suspend fun setStatus(workspaceId: String, itemId: String, status: String, version: Int): Item =
|
||||
call(
|
||||
path = "/workspaces/$workspaceId/items/$itemId/status",
|
||||
body = json.encodeToString(mapOf("status" to status, "version" to version)),
|
||||
body = json.encodeToString(StatusRequest(status, version)),
|
||||
auth = true,
|
||||
)
|
||||
|
||||
@@ -248,7 +340,7 @@ class Api(private val tokens: TokenStore) {
|
||||
|
||||
suspend fun addComment(workspaceId: String, itemId: String, body: String): Activity = call(
|
||||
path = "/workspaces/$workspaceId/items/$itemId/comments",
|
||||
body = json.encodeToString(mapOf("body_md" to body)),
|
||||
body = json.encodeToString(CommentRequest(body)),
|
||||
auth = true,
|
||||
)
|
||||
|
||||
@@ -266,10 +358,10 @@ class Api(private val tokens: TokenStore) {
|
||||
workspaceId: String,
|
||||
itemId: String,
|
||||
action: String,
|
||||
body: Map<String, Any> = emptyMap(),
|
||||
body: String = "{}",
|
||||
): Ticket = call(
|
||||
path = "/workspaces/$workspaceId/items/$itemId/ticket/$action",
|
||||
body = json.encodeToString(body),
|
||||
body = body,
|
||||
auth = true,
|
||||
)
|
||||
|
||||
@@ -283,7 +375,7 @@ class Api(private val tokens: TokenStore) {
|
||||
workspaceId,
|
||||
itemId,
|
||||
"verify",
|
||||
mapOf("approve" to approve, "note" to note, "resolution_md" to resolution),
|
||||
json.encodeToString(VerifyRequest(approve, note, resolution)),
|
||||
)
|
||||
|
||||
/** 把任务升级成工单:原任务留在原地变"被阻塞",不搬数据。 */
|
||||
@@ -294,15 +386,67 @@ class Api(private val tokens: TokenStore) {
|
||||
version: Int? = null,
|
||||
): JsonElement = call(
|
||||
path = "/workspaces/$workspaceId/items/$itemId/promote",
|
||||
body = json.encodeToString(
|
||||
buildMap {
|
||||
put("assignee_id", assigneeId)
|
||||
if (version != null) put("version", version)
|
||||
},
|
||||
),
|
||||
body = json.encodeToString(PromoteRequest(assigneeId, version)),
|
||||
auth = true,
|
||||
)
|
||||
|
||||
// ---- M3 知识 ----
|
||||
|
||||
/** status: draft / in_review / published / archived,多个用逗号拼说服务端是重复参数。 */
|
||||
suspend fun knowledge(
|
||||
workspaceId: String,
|
||||
statuses: List<String> = listOf("draft", "in_review"),
|
||||
query: String? = null,
|
||||
): KnowledgePage {
|
||||
val params = buildString {
|
||||
append("?limit=30")
|
||||
statuses.forEach { append("&status=").append(it) }
|
||||
if (!query.isNullOrBlank()) append("&q=").append(java.net.URLEncoder.encode(query, "UTF-8"))
|
||||
}
|
||||
return call(path = "/workspaces/$workspaceId/knowledge$params", auth = true)
|
||||
}
|
||||
|
||||
/** 手记:没有来源的知识,得自己写清楚。 */
|
||||
suspend fun createKnowledge(workspaceId: String, title: String, body: String = ""): KnowledgeDoc =
|
||||
call(
|
||||
path = "/workspaces/$workspaceId/knowledge",
|
||||
body = json.encodeToString(KnowledgeCreateRequest(title, body, "note")),
|
||||
auth = true,
|
||||
)
|
||||
|
||||
/** 把一件事沉淀成草稿。工单走复盘,任务走做法。 */
|
||||
suspend fun sediment(workspaceId: String, itemId: String, docType: String = "howto"): KnowledgeDoc =
|
||||
call(
|
||||
path = "/workspaces/$workspaceId/items/$itemId/knowledge?doc_type=$docType",
|
||||
body = "{}",
|
||||
auth = true,
|
||||
)
|
||||
|
||||
/** 知识动作:submit_review / withdraw / publish / archive / restore。 */
|
||||
suspend fun knowledgeAction(
|
||||
workspaceId: String,
|
||||
itemId: String,
|
||||
action: String,
|
||||
): KnowledgeDoc = call(
|
||||
path = "/workspaces/$workspaceId/knowledge/$itemId/status",
|
||||
body = json.encodeToString(KnowledgeActionRequest(action)),
|
||||
auth = true,
|
||||
)
|
||||
|
||||
/** 改标题/正文必须带 version;已发布的内容服务端会拒改。 */
|
||||
suspend fun updateKnowledge(
|
||||
workspaceId: String,
|
||||
itemId: String,
|
||||
version: Int,
|
||||
title: String? = null,
|
||||
body: String? = null,
|
||||
): KnowledgeDoc = call(
|
||||
path = "/workspaces/$workspaceId/knowledge/$itemId",
|
||||
body = json.encodeToString(KnowledgeUpdateRequest(version, title, body)),
|
||||
auth = true,
|
||||
method = "PATCH",
|
||||
)
|
||||
|
||||
private suspend inline fun <reified T> call(
|
||||
path: String,
|
||||
body: String? = null,
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
package com.dadao.task.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.dadao.task.data.Api
|
||||
import com.dadao.task.data.ApiException
|
||||
import com.dadao.task.data.KnowledgeDoc
|
||||
import com.dadao.task.data.Workspace
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private val KNOWLEDGE_TABS = listOf(
|
||||
"待核对" to listOf("draft", "in_review"),
|
||||
"已发布" to listOf("published"),
|
||||
"已归档" to listOf("archived"),
|
||||
"全部" to listOf("draft", "in_review", "published", "archived"),
|
||||
)
|
||||
|
||||
private val DOC_TYPE_LABEL = mapOf(
|
||||
"note" to "手记",
|
||||
"howto" to "做法",
|
||||
"faq" to "问答",
|
||||
"postmortem" to "复盘",
|
||||
"decision" to "决策",
|
||||
)
|
||||
|
||||
private val KNOWLEDGE_STATUS_LABEL = mapOf(
|
||||
"draft" to "草稿",
|
||||
"in_review" to "待审议",
|
||||
"published" to "已发布",
|
||||
"archived" to "已归档",
|
||||
)
|
||||
|
||||
/**
|
||||
* 知识列表。
|
||||
*
|
||||
* 手机端做知识,只保留两件最有价值的事:**记一条**(通勤路上想到就写)
|
||||
* 与**核对后发布**(草稿躺在待核对里,手机上顺手就能审)。
|
||||
* 版本历史、标签管理这类重活留给桌面端,别把手机塞成缩小版 Web。
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun KnowledgeScreen(
|
||||
api: Api,
|
||||
workspace: Workspace,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val docs = remember { mutableStateListOf<KnowledgeDoc>() }
|
||||
var tab by remember { mutableStateOf(KNOWLEDGE_TABS.first()) }
|
||||
var loading by remember { mutableStateOf(true) }
|
||||
var error by remember { mutableStateOf<String?>(null) }
|
||||
var open by remember { mutableStateOf<KnowledgeDoc?>(null) }
|
||||
var composing by remember { mutableStateOf(false) }
|
||||
var draftTitle by remember { mutableStateOf("") }
|
||||
var draftBody by remember { mutableStateOf("") }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
suspend fun reload() {
|
||||
loading = true
|
||||
error = null
|
||||
try {
|
||||
val page = api.knowledge(workspace.id, tab.second)
|
||||
docs.clear()
|
||||
docs.addAll(page.items)
|
||||
} catch (e: ApiException) {
|
||||
error = e.message
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(tab) { reload() }
|
||||
|
||||
suspend fun act(doc: KnowledgeDoc, action: String) {
|
||||
try {
|
||||
val updated = api.knowledgeAction(workspace.id, doc.id, action)
|
||||
open = updated
|
||||
reload()
|
||||
} catch (e: ApiException) {
|
||||
// 7004=权限不够(发布要 owner/admin),7003=状态已变
|
||||
error = when (e.code) {
|
||||
7004 -> "发布要管理员或 owner 来点"
|
||||
7003 -> "这个状态不允许该操作,可能刚被别人改过"
|
||||
else -> e.message
|
||||
}
|
||||
reload()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun save() {
|
||||
val title = draftTitle.trim()
|
||||
if (title.isEmpty()) return
|
||||
try {
|
||||
api.createKnowledge(workspace.id, title, draftBody.trim())
|
||||
draftTitle = ""
|
||||
draftBody = ""
|
||||
composing = false
|
||||
tab = KNOWLEDGE_TABS.first()
|
||||
reload()
|
||||
} catch (e: ApiException) {
|
||||
error = e.message
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("${workspace.name} · 知识") },
|
||||
navigationIcon = { TextButton(onClick = onBack) { Text("返回") } },
|
||||
actions = {
|
||||
TextButton(onClick = { composing = !composing }) {
|
||||
Text(if (composing) "收起" else "记一条")
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
Column(Modifier.fillMaxSize().padding(padding).padding(horizontal = 16.dp)) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
KNOWLEDGE_TABS.forEach { entry ->
|
||||
FilterChip(
|
||||
selected = tab == entry,
|
||||
onClick = { tab = entry },
|
||||
label = { Text(entry.first) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (composing) {
|
||||
OutlinedTextField(
|
||||
value = draftTitle,
|
||||
onValueChange = { draftTitle = it },
|
||||
label = { Text("标题:这件事叫什么") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = draftBody,
|
||||
onValueChange = { draftBody = it },
|
||||
label = { Text("正文:以后还会用到的那部分") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Button(onClick = { scope.launch { save() } }, enabled = draftTitle.isNotBlank()) {
|
||||
Text("存成草稿")
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
error?.let {
|
||||
Text(it, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
when {
|
||||
loading -> Text("加载中…")
|
||||
docs.isEmpty() -> Text(
|
||||
if (tab.first == "待核对") {
|
||||
"没有待核对的知识。工单验收通过会自动生成一份草稿,等一句话就能发。"
|
||||
} else {
|
||||
"这里还没有内容。"
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
else -> LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
items(docs, key = { it.id }) { doc ->
|
||||
Card(
|
||||
onClick = { open = doc },
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 2.dp),
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth().padding(12.dp)) {
|
||||
Text(doc.title, style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
buildString {
|
||||
append(DOC_TYPE_LABEL[doc.knowledge.docType] ?: doc.knowledge.docType)
|
||||
append(" · ")
|
||||
append(KNOWLEDGE_STATUS_LABEL[doc.status] ?: doc.status)
|
||||
val source = doc.source
|
||||
if (source != null && source.title.isNotBlank()) {
|
||||
append(" · 来自")
|
||||
append(if (source.kind == "ticket") "工单" else "任务")
|
||||
append("「").append(source.title).append("」")
|
||||
} else {
|
||||
append(" · 手记")
|
||||
}
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val selected = open
|
||||
if (selected != null) {
|
||||
KnowledgeDialog(
|
||||
doc = selected,
|
||||
onClose = { open = null },
|
||||
onAction = { action -> scope.launch { act(selected, action) } },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KnowledgeDialog(
|
||||
doc: KnowledgeDoc,
|
||||
onClose: () -> Unit,
|
||||
onAction: (String) -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onClose,
|
||||
title = { Text(doc.title) },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
buildString {
|
||||
append(KNOWLEDGE_STATUS_LABEL[doc.status] ?: doc.status)
|
||||
doc.knowledge.publishedAt?.let { append(" · 发布于 ").append(it.take(16).replace("T", " ")) }
|
||||
if (doc.knowledge.generatedBy == "rule") append(" · 由系统整理,请核对后再发布")
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
doc.bodyMd.ifBlank { "(还没有正文)" },
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
when (doc.status) {
|
||||
"draft" -> {
|
||||
TextButton(onClick = { onAction("submit_review") }) { Text("提交审议") }
|
||||
TextButton(onClick = { onAction("publish") }) { Text("发布") }
|
||||
}
|
||||
"in_review" -> {
|
||||
TextButton(onClick = { onAction("withdraw") }) { Text("撤回") }
|
||||
TextButton(onClick = { onAction("publish") }) { Text("发布") }
|
||||
}
|
||||
"published" -> TextButton(onClick = { onAction("archive") }) { Text("归档") }
|
||||
"archived" -> TextButton(onClick = { onAction("restore") }) { Text("恢复草稿") }
|
||||
}
|
||||
TextButton(onClick = onClose) { Text("关闭") }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -73,6 +73,7 @@ fun TaskListScreen(
|
||||
workspace: Workspace,
|
||||
onBack: () -> Unit,
|
||||
onOpenTickets: () -> Unit = {},
|
||||
onOpenKnowledge: () -> Unit = {},
|
||||
) {
|
||||
val items = remember { mutableStateListOf<Item>() }
|
||||
var view by remember { mutableStateOf("today") }
|
||||
@@ -128,7 +129,10 @@ fun TaskListScreen(
|
||||
TopAppBar(
|
||||
title = { Text(workspace.name) },
|
||||
navigationIcon = { TextButton(onClick = onBack) { Text("返回") } },
|
||||
actions = { TextButton(onClick = onOpenTickets) { Text("工单") } },
|
||||
actions = {
|
||||
TextButton(onClick = onOpenTickets) { Text("工单") }
|
||||
TextButton(onClick = onOpenKnowledge) { Text("知识") }
|
||||
},
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
|
||||
Reference in New Issue
Block a user