优化 UI 细节
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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()) }
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user