优化 UI 细节

This commit is contained in:
amos
2025-12-17 10:49:42 +08:00
parent b05d53ab77
commit 422d1d0b2d
5 changed files with 286 additions and 193 deletions

View File

@@ -53,6 +53,10 @@ fun ArchiveScreen(
// Get recent posts for the list
val recentPosts by viewModel.recentPosts.collectAsState()
// 控制是否展开显示所有帖子
var isExpanded by remember { mutableStateOf(false) }
val displayPosts = if (isExpanded) recentPosts else recentPosts.take(4)
LaunchedEffect(Unit) {
viewModel.loadHeatmap()
@@ -164,13 +168,39 @@ fun ArchiveScreen(
}
}
// Recent posts list - 显示所有帖子,可滚动查看
items(recentPosts) { post ->
// Recent posts list - 默认显示4条
items(displayPosts) { post ->
DatePostItem(
post = post,
onClick = { onPostClick(post.id) }
)
}
// 展开/收起按钮
if (recentPosts.size > 4) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
contentAlignment = Alignment.Center
) {
TextButton(
onClick = { isExpanded = !isExpanded },
shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults.textButtonColors(
contentColor = Brand500
)
) {
Text(
text = if (isExpanded) "收起" else "查看更多 (${recentPosts.size - 4}条)",
fontSize = 14.sp,
fontWeight = FontWeight.Medium
)
}
}
}
}
}
}
}
@@ -337,11 +367,17 @@ private fun QuarterHeatmapGrid(
val cellSize = 16.dp
val spacing = 3.dp
Column(modifier = Modifier.fillMaxWidth()) {
// Month Labels Row
// 计算热力图总宽度用于月份标签对齐
val gridWidthDp = (weeks.size * 19 - 3).dp // 16 + 3 = 19, 最后减去一个 spacing
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Month Labels Row - 居中并与热力图对齐
Row(
modifier = Modifier
.fillMaxWidth()
.width(gridWidthDp)
.padding(bottom = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
@@ -350,16 +386,14 @@ private fun QuarterHeatmapGrid(
text = "${month}",
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
color = Slate400,
modifier = Modifier.weight(1f)
color = Slate400
)
}
}
// Heatmap Grid (no week labels, just cells)
// Heatmap Grid (no week labels, just cells) - 居中显示
Row(
horizontalArrangement = Arrangement.spacedBy(spacing),
modifier = Modifier.fillMaxWidth()
horizontalArrangement = Arrangement.spacedBy(spacing)
) {
weeks.forEach { week ->
Column(verticalArrangement = Arrangement.spacedBy(spacing)) {
@@ -401,6 +435,17 @@ private fun QuarterHeatmapGrid(
Spacer(modifier = Modifier.width(4.dp))
}
Text(text = "", fontSize = 10.sp, color = Slate400)
Spacer(modifier = Modifier.width(16.dp))
// 未来日期图例
Box(
modifier = Modifier
.size(10.dp)
.clip(RoundedCornerShape(2.dp))
.background(FutureGray)
.border(1.dp, Color(0xFFB0B8C0), RoundedCornerShape(2.dp))
)
Spacer(modifier = Modifier.width(4.dp))
Text(text = "未来", fontSize = 10.sp, color = Slate400)
}
}
}
@@ -426,20 +471,36 @@ private fun HeatmapCell(
.size(size)
.clip(RoundedCornerShape(3.dp))
.background(getHeatmapColor(level))
.then(
if (isFuture) {
// 未来日期添加斜线边框效果
Modifier.border(1.dp, Color(0xFFB0B8C0), RoundedCornerShape(3.dp))
} else {
Modifier
}
)
.clickable(enabled = count > 0 && !isFuture) { onClick() }
)
}
// GitHub 风格绿色
private val GitHubGreen0 = Color(0xFFEBEDF0) // 无记录
private val GitHubGreen1 = Color(0xFF9BE9A8) // 少
private val GitHubGreen2 = Color(0xFF40C463) // 中
private val GitHubGreen3 = Color(0xFF30A14E) // 多
private val GitHubGreen4 = Color(0xFF216E39) // 最多
private val FutureGray = Color(0xFFD0D7DE) // 未来日期 - 斜线纹理色
@Composable
private fun getHeatmapColor(level: Int): Color {
return when (level) {
-1 -> Slate100.copy(alpha = 0.3f)
0 -> Slate100
1 -> Brand100
2 -> Brand500.copy(alpha = 0.5f)
3 -> Brand500
4 -> Brand900
else -> Slate100
-1 -> FutureGray // 未来日期用明显的灰色
0 -> GitHubGreen0
1 -> GitHubGreen1
2 -> GitHubGreen2
3 -> GitHubGreen3
4 -> GitHubGreen4
else -> GitHubGreen0
}
}

View File

@@ -1,6 +1,7 @@
package com.memory.app.ui.screen
import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
@@ -35,6 +36,11 @@ fun CreatePostScreen(
onPost: (String, List<Uri>) -> Unit,
isLoading: Boolean
) {
// 拦截系统返回手势
BackHandler(enabled = true) {
onClose()
}
var content by remember { mutableStateOf("") }
var selectedImages by remember { mutableStateOf<List<Uri>>(emptyList()) }
var showEmojiPicker by remember { mutableStateOf(false) }

View File

@@ -1,6 +1,7 @@
package com.memory.app.ui.screen
import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
@@ -35,6 +36,11 @@ fun EditPostScreen(
onSave: (String, List<String>, List<Uri>) -> Unit,
isLoading: Boolean
) {
// 拦截系统返回手势
BackHandler(enabled = true) {
onClose()
}
var content by remember { mutableStateOf(post.content) }
var existingImages by remember { mutableStateOf(post.media.map { it.mediaUrl }) }
var newImages by remember { mutableStateOf<List<Uri>>(emptyList()) }

View File

@@ -47,186 +47,200 @@ fun SearchScreen(
val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
Column(
// 切换到其他 tab 时清空搜索结果
DisposableEffect(Unit) {
onDispose {
viewModel.clearResults()
}
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
// Header
Column(
modifier = Modifier
.fillMaxWidth()
.background(Color.White)
.statusBarsPadding()
.padding(horizontal = 20.dp, vertical = 16.dp)
) {
// Title
Text(
text = "搜索",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = Slate900,
modifier = Modifier.padding(bottom = 16.dp)
)
item {
Column(
modifier = Modifier
.fillMaxWidth()
.background(Color.White)
.statusBarsPadding()
.padding(horizontal = 20.dp, vertical = 16.dp)
) {
// Title
Text(
text = "搜索",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = Slate900,
modifier = Modifier.padding(bottom = 20.dp)
)
// Search Input
OutlinedTextField(
value = searchQuery,
onValueChange = { viewModel.updateQuery(it) },
placeholder = { Text("搜索内容关键词...", color = Slate400, fontSize = 15.sp) },
leadingIcon = {
// Search Input
OutlinedTextField(
value = searchQuery,
onValueChange = { viewModel.updateQuery(it) },
placeholder = { Text("搜索内容关键词...", color = Slate400, fontSize = 15.sp) },
leadingIcon = {
Icon(
Icons.Outlined.Search,
contentDescription = null,
tint = Slate400,
modifier = Modifier.size(20.dp)
)
},
modifier = Modifier
.fillMaxWidth()
.shadow(2.dp, RoundedCornerShape(16.dp)),
shape = RoundedCornerShape(16.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = Slate50,
unfocusedContainerColor = Slate50,
focusedBorderColor = Brand500,
unfocusedBorderColor = Slate100,
cursorColor = Brand500
),
singleLine = true
)
Spacer(modifier = Modifier.height(16.dp))
// Date Range Row
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Start Date
Box(
modifier = Modifier
.weight(1f)
.clickable { showStartDatePicker = true }
) {
OutlinedTextField(
value = startDate?.format(dateFormatter) ?: "",
onValueChange = { },
placeholder = { Text("开始日期", color = Slate400, fontSize = 14.sp) },
leadingIcon = {
Icon(
Icons.Outlined.CalendarMonth,
contentDescription = null,
tint = Slate400,
modifier = Modifier.size(18.dp)
)
},
modifier = Modifier
.fillMaxWidth()
.shadow(2.dp, RoundedCornerShape(16.dp)),
shape = RoundedCornerShape(16.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = Slate50,
unfocusedContainerColor = Slate50,
focusedBorderColor = Slate100,
unfocusedBorderColor = Slate100,
disabledContainerColor = Slate50,
disabledBorderColor = Slate100,
disabledLeadingIconColor = Slate400,
disabledPlaceholderColor = Slate400,
disabledTextColor = Slate900
),
readOnly = true,
enabled = false,
singleLine = true
)
}
// End Date
Box(
modifier = Modifier
.weight(1f)
.clickable { showEndDatePicker = true }
) {
OutlinedTextField(
value = endDate?.format(dateFormatter) ?: "",
onValueChange = { },
placeholder = { Text("结束日期", color = Slate400, fontSize = 14.sp) },
leadingIcon = {
Icon(
Icons.Outlined.CalendarMonth,
contentDescription = null,
tint = Slate400,
modifier = Modifier.size(18.dp)
)
},
modifier = Modifier
.fillMaxWidth()
.shadow(2.dp, RoundedCornerShape(16.dp)),
shape = RoundedCornerShape(16.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = Slate50,
unfocusedContainerColor = Slate50,
focusedBorderColor = Slate100,
unfocusedBorderColor = Slate100,
disabledContainerColor = Slate50,
disabledBorderColor = Slate100,
disabledLeadingIconColor = Slate400,
disabledPlaceholderColor = Slate400,
disabledTextColor = Slate900
),
readOnly = true,
enabled = false,
singleLine = true
)
}
}
Spacer(modifier = Modifier.height(20.dp))
// Search Button
Button(
onClick = { viewModel.search() },
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.shadow(8.dp, RoundedCornerShape(16.dp), spotColor = Brand500.copy(alpha = 0.2f)),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Brand500,
contentColor = Color.White
)
) {
Icon(
Icons.Outlined.Search,
contentDescription = null,
tint = Slate400,
modifier = Modifier.size(20.dp)
)
},
modifier = Modifier
.fillMaxWidth()
.shadow(2.dp, RoundedCornerShape(16.dp)),
shape = RoundedCornerShape(16.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = Slate50,
unfocusedContainerColor = Slate50,
focusedBorderColor = Brand500,
unfocusedBorderColor = Slate100,
cursorColor = Brand500
),
singleLine = true
)
Spacer(modifier = Modifier.height(16.dp))
// Date Range Row
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Start Date
Box(
modifier = Modifier
.weight(1f)
.clickable { showStartDatePicker = true }
) {
OutlinedTextField(
value = startDate?.format(dateFormatter) ?: "",
onValueChange = { },
placeholder = { Text("开始日期", color = Slate400, fontSize = 14.sp) },
leadingIcon = {
Icon(
Icons.Outlined.CalendarMonth,
contentDescription = null,
tint = Slate400,
modifier = Modifier.size(18.dp)
)
},
modifier = Modifier
.fillMaxWidth()
.shadow(2.dp, RoundedCornerShape(16.dp)),
shape = RoundedCornerShape(16.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = Slate50,
unfocusedContainerColor = Slate50,
focusedBorderColor = Slate100,
unfocusedBorderColor = Slate100,
disabledContainerColor = Slate50,
disabledBorderColor = Slate100,
disabledLeadingIconColor = Slate400,
disabledPlaceholderColor = Slate400,
disabledTextColor = Slate900
),
readOnly = true,
enabled = false,
singleLine = true
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "搜 索",
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
}
// End Date
Box(
modifier = Modifier
.weight(1f)
.clickable { showEndDatePicker = true }
) {
OutlinedTextField(
value = endDate?.format(dateFormatter) ?: "",
onValueChange = { },
placeholder = { Text("结束日期", color = Slate400, fontSize = 14.sp) },
leadingIcon = {
Icon(
Icons.Outlined.CalendarMonth,
contentDescription = null,
tint = Slate400,
modifier = Modifier.size(18.dp)
)
},
modifier = Modifier
.fillMaxWidth()
.shadow(2.dp, RoundedCornerShape(16.dp)),
shape = RoundedCornerShape(16.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = Slate50,
unfocusedContainerColor = Slate50,
focusedBorderColor = Slate100,
unfocusedBorderColor = Slate100,
disabledContainerColor = Slate50,
disabledBorderColor = Slate100,
disabledLeadingIconColor = Slate400,
disabledPlaceholderColor = Slate400,
disabledTextColor = Slate900
),
readOnly = true,
enabled = false,
singleLine = true
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Search Button
Button(
onClick = { viewModel.search() },
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
.shadow(8.dp, RoundedCornerShape(16.dp), spotColor = Brand500.copy(alpha = 0.2f)),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Brand500,
contentColor = Color.White
)
) {
Icon(
Icons.Outlined.Search,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "搜 索",
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
}
}
// Results Area
Box(modifier = Modifier.fillMaxSize()) {
when {
isLoading -> {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
color = Brand500
)
when {
isLoading -> {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(300.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(color = Brand500)
}
}
posts.isEmpty() -> {
// Empty State
}
posts.isEmpty() -> {
// Empty State
item {
Column(
modifier = Modifier
.align(Alignment.Center)
.padding(top = 40.dp),
.fillMaxWidth()
.padding(top = 80.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
@@ -256,22 +270,21 @@ fun SearchScreen(
)
}
}
else -> {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 100.dp)
) {
items(posts, key = { it.id }) { post ->
PostCard(
post = post,
onPostClick = { onPostClick(post.id) },
onLikeClick = { },
onCommentClick = { onPostClick(post.id) },
onReactionClick = { }
)
HorizontalDivider(color = Slate100, thickness = 1.dp)
}
}
}
else -> {
items(posts, key = { it.id }) { post ->
PostCard(
post = post,
onPostClick = { onPostClick(post.id) },
onLikeClick = { },
onCommentClick = { onPostClick(post.id) },
onReactionClick = { }
)
HorizontalDivider(color = Slate100, thickness = 1.dp)
}
// 底部留白
item {
Spacer(modifier = Modifier.height(100.dp))
}
}
}

View File

@@ -45,6 +45,13 @@ class SearchViewModel : ViewModel() {
_endDate.value = null
}
fun clearResults() {
_posts.value = emptyList()
_searchQuery.value = ""
_startDate.value = null
_endDate.value = null
}
fun search() {
viewModelScope.launch {
_isLoading.value = true