feat: Implement Design System screen and enhance UI/UX

This commit introduces a new "Design System" screen for developers and designers, refines navigation animations for a smoother experience, and improves typography consistency across various UI components.

**Key Changes:**

*   **feat(Design System):**
    *   Added a new `DesignSystemScreen.kt` to visually showcase the app's color palette, typography styles, and shape system.
    *   The screen features animated guides and detailed specifications for each design token (e.g., font size, weight, color roles).
    *   A new "About Tangyuan Design System" menu item has been added to the `UserScreen` (for both logged-in and logged-out states), navigating to this new screen.

*   **refactor(Navigation):**
    *   Replaced default navigation transitions with custom animations using `CubicBezierEasing` for a more fluid and responsive feel (e.g., quick spring and fade effects).
    *   Detail screens (`PostDetail`, `UserDetail`, `ImageDetail`) now use a `fadeIn`/`fadeOut` transition to prevent conflicts with shared element animations.
    *   Side-panel screens (`About`, `DesignSystem`) now use a horizontal slide transition.
    *   Navigation between main tabs (`Talk`, `Message`, `User`) is now animated with a horizontal slide and fade.

*   **refactor(Typography & UI):**
    *   Standardized the font weight for titles and important text to `SemiBold` across `PostCardItem`, `PostDetailScreen`, and `CommentComponents` for better visual hierarchy.
    *   Updated the `PostDetailScreen` top bar title to "帖子详情" for clarity.
    *   Replaced the literary font with the general-purpose font in some UI elements like comment interaction labels for improved readability.
    *   Enabled image click navigation from the `UserDetailScreen`'s post feed.

*   **feat(Create Post):**
    *   Introduced `CreatePostDto.kt` and `CreatePostRepository.kt` to support post creation.
    *   The repository now handles fetching categories and creating posts through a two-step process: creating post metadata and then the post body.
    *   Added `CreatePostState` to manage the UI state for the post creation screen, including validation for content length and image limits.
This commit is contained in:
grtsinry43 2025-10-08 17:16:33 +08:00
parent c5ec5b1a0b
commit a528b623b9
Signed by: grtsinry43
GPG Key ID: F3305FB3A978C934
12 changed files with 1103 additions and 28 deletions

View File

@ -4,7 +4,7 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-10-06T14:55:34.827294Z">
<DropdownSelection timestamp="2025-10-08T05:47:59.421656019Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=6fbe7ac" />

13
.idea/deviceManager.xml generated Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

View File

@ -21,6 +21,7 @@ import com.qingshuige.tangyuan.ui.components.PageLevel
import com.qingshuige.tangyuan.ui.components.TangyuanBottomAppBar
import com.qingshuige.tangyuan.ui.components.TangyuanTopBar
import com.qingshuige.tangyuan.ui.screens.AboutScreen
import com.qingshuige.tangyuan.ui.screens.DesignSystemScreen
import com.qingshuige.tangyuan.ui.screens.PostDetailScreen
import com.qingshuige.tangyuan.ui.screens.ImageDetailScreen
import com.qingshuige.tangyuan.ui.screens.TalkScreen
@ -29,6 +30,10 @@ import com.qingshuige.tangyuan.ui.screens.UserDetailScreen
import com.qingshuige.tangyuan.ui.screens.UserScreen
import com.qingshuige.tangyuan.viewmodel.UserViewModel
// 自定义带回弹效果的easing - 快速流畅
private val QuickSpringEasing = CubicBezierEasing(0.34f, 1.3f, 0.64f, 1.0f)
private val QuickEasing = CubicBezierEasing(0.2f, 0.0f, 0.0f, 1.0f)
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun App() {
@ -53,7 +58,8 @@ fun App() {
},
onAboutClick = { navController.navigate(Screen.About.route) },
sharedTransitionScope = this@SharedTransitionLayout,
animatedContentScope = this@composable
animatedContentScope = this@composable,
onDesignSystemClick = { navController.navigate(Screen.DesignSystem.route) }
)
}
@ -63,8 +69,8 @@ fun App() {
slideInVertically(
initialOffsetY = { it },
animationSpec = tween(
durationMillis = 800,
easing = FastOutSlowInEasing
durationMillis = 350,
easing = QuickSpringEasing
)
)
},
@ -72,8 +78,8 @@ fun App() {
slideOutVertically(
targetOffsetY = { it },
animationSpec = tween(
durationMillis = 600,
easing = FastOutSlowInEasing
durationMillis = 250,
easing = QuickEasing
)
)
},
@ -81,8 +87,8 @@ fun App() {
slideOutVertically(
targetOffsetY = { it },
animationSpec = tween(
durationMillis = 600,
easing = FastOutSlowInEasing
durationMillis = 250,
easing = QuickEasing
)
)
}
@ -90,12 +96,44 @@ fun App() {
LoginScreen(navController = navController)
}
// 帖子详情页
// 帖子详情页 - 使用淡入淡出避免与共享元素冲突
composable(
route = Screen.PostDetail.route,
arguments = listOf(
navArgument("postId") { type = NavType.IntType }
)
),
enterTransition = {
fadeIn(
animationSpec = tween(
durationMillis = 300,
easing = QuickEasing
)
)
},
exitTransition = {
fadeOut(
animationSpec = tween(
durationMillis = 200,
easing = QuickEasing
)
)
},
popEnterTransition = {
fadeIn(
animationSpec = tween(
durationMillis = 200,
easing = QuickEasing
)
)
},
popExitTransition = {
fadeOut(
animationSpec = tween(
durationMillis = 300,
easing = QuickEasing
)
)
}
) { backStackEntry ->
val postId = backStackEntry.arguments?.getInt("postId") ?: 0
@ -118,13 +156,45 @@ fun App() {
)
}
// 图片详情页
// 图片详情页 - 使用淡入淡出避免与共享元素冲突
composable(
route = Screen.ImageDetail.route,
arguments = listOf(
navArgument("postId") { type = NavType.IntType },
navArgument("imageIndex") { type = NavType.IntType }
)
),
enterTransition = {
fadeIn(
animationSpec = tween(
durationMillis = 300,
easing = QuickEasing
)
)
},
exitTransition = {
fadeOut(
animationSpec = tween(
durationMillis = 200,
easing = QuickEasing
)
)
},
popEnterTransition = {
fadeIn(
animationSpec = tween(
durationMillis = 200,
easing = QuickEasing
)
)
},
popExitTransition = {
fadeOut(
animationSpec = tween(
durationMillis = 300,
easing = QuickEasing
)
)
}
) { backStackEntry ->
val postId = backStackEntry.arguments?.getInt("postId") ?: 0
val imageIndex = backStackEntry.arguments?.getInt("imageIndex") ?: 0
@ -149,12 +219,44 @@ fun App() {
)
}
// 用户详情页
// 用户详情页 - 使用淡入淡出避免与共享元素冲突
composable(
route = Screen.UserDetail.route,
arguments = listOf(
navArgument("userId") { type = NavType.IntType }
)
),
enterTransition = {
fadeIn(
animationSpec = tween(
durationMillis = 300,
easing = QuickEasing
)
)
},
exitTransition = {
fadeOut(
animationSpec = tween(
durationMillis = 200,
easing = QuickEasing
)
)
},
popEnterTransition = {
fadeIn(
animationSpec = tween(
durationMillis = 200,
easing = QuickEasing
)
)
},
popExitTransition = {
fadeOut(
animationSpec = tween(
durationMillis = 300,
easing = QuickEasing
)
)
}
) { backStackEntry ->
val userId = backStackEntry.arguments?.getInt("userId") ?: 0
@ -164,6 +266,14 @@ fun App() {
onPostClick = { postId ->
navController.navigate(Screen.PostDetail.createRoute(postId))
},
onImageClick = { postId, imageIndex ->
navController.navigate(Screen.ImageDetail.createRoute(postId, imageIndex)) {
popUpTo(Screen.PostDetail.createRoute(postId)) {
inclusive = true
}
launchSingleTop = true
}
},
onFollowClick = {
// TODO: 实现关注功能
},
@ -172,11 +282,93 @@ fun App() {
)
}
composable(Screen.About.route) {
composable(
route = Screen.About.route,
enterTransition = {
slideInHorizontally(
initialOffsetX = { it },
animationSpec = tween(
durationMillis = 300,
easing = QuickSpringEasing
)
)
},
exitTransition = {
slideOutHorizontally(
targetOffsetX = { -it / 3 },
animationSpec = tween(
durationMillis = 300,
easing = QuickEasing
)
)
},
popEnterTransition = {
slideInHorizontally(
initialOffsetX = { -it / 3 },
animationSpec = tween(
durationMillis = 300,
easing = QuickEasing
)
)
},
popExitTransition = {
slideOutHorizontally(
targetOffsetX = { it },
animationSpec = tween(
durationMillis = 250,
easing = QuickEasing
)
)
}
) {
AboutScreen(
onBackClick = { navController.popBackStack() }
)
}
composable(
route = Screen.DesignSystem.route,
enterTransition = {
slideInHorizontally(
initialOffsetX = { it },
animationSpec = tween(
durationMillis = 300,
easing = QuickSpringEasing
)
)
},
exitTransition = {
slideOutHorizontally(
targetOffsetX = { -it / 3 },
animationSpec = tween(
durationMillis = 300,
easing = QuickEasing
)
)
},
popEnterTransition = {
slideInHorizontally(
initialOffsetX = { -it / 3 },
animationSpec = tween(
durationMillis = 300,
easing = QuickEasing
)
)
},
popExitTransition = {
slideOutHorizontally(
targetOffsetX = { it },
animationSpec = tween(
durationMillis = 250,
easing = QuickEasing
)
)
}
) {
DesignSystemScreen(
onBackClick = { navController.popBackStack() }
)
}
}
}
}
@ -189,6 +381,7 @@ fun MainFlow(
onImageClick: (Int, Int) -> Unit = { _, _ -> },
onAuthorClick: (Int) -> Unit = {},
onAboutClick: () -> Unit,
onDesignSystemClick: () -> Unit,
sharedTransitionScope: SharedTransitionScope? = null,
animatedContentScope: AnimatedContentScope? = null,
userViewModel: UserViewModel = hiltViewModel()
@ -254,7 +447,63 @@ fun MainFlow(
NavHost(
navController = mainNavController,
startDestination = Screen.Talk.route,
modifier = Modifier.padding(innerPadding)
modifier = Modifier.padding(innerPadding),
enterTransition = {
slideInHorizontally(
initialOffsetX = { it / 2 },
animationSpec = tween(
durationMillis = 200,
easing = QuickEasing
)
) + fadeIn(
animationSpec = tween(
durationMillis = 200,
easing = QuickEasing
)
)
},
exitTransition = {
slideOutHorizontally(
targetOffsetX = { -it / 2 },
animationSpec = tween(
durationMillis = 200,
easing = QuickEasing
)
) + fadeOut(
animationSpec = tween(
durationMillis = 200,
easing = QuickEasing
)
)
},
popEnterTransition = {
slideInHorizontally(
initialOffsetX = { -it / 2 },
animationSpec = tween(
durationMillis = 200,
easing = QuickEasing
)
) + fadeIn(
animationSpec = tween(
durationMillis = 200,
easing = QuickEasing
)
)
},
popExitTransition = {
slideOutHorizontally(
targetOffsetX = { it / 2 },
animationSpec = tween(
durationMillis = 200,
easing = QuickEasing
)
) + fadeOut(
animationSpec = tween(
durationMillis = 200,
easing = QuickEasing
)
)
}
) {
composable(Screen.Talk.route) {
TalkScreen(
@ -278,7 +527,8 @@ fun MainFlow(
onSettings = {
// TODO: 导航到设置页面
},
onAbout = onAboutClick
onAbout = onAboutClick,
onDesignSystem = onDesignSystemClick,
)
}
}

View File

@ -0,0 +1,67 @@
package com.qingshuige.tangyuan.model
import java.util.Date
data class CreatePostDto(
val textContent: String,
val categoryId: Int,
val sectionId: Int, // 0 或 1
val isVisible: Boolean = true,
val imageUUIDs: List<String> = emptyList()
) {
fun toCreatPostMetadataDto(userId: Int): CreatPostMetadataDto {
return CreatPostMetadataDto(
isVisible = isVisible,
postDateTime = Date(),
sectionId = sectionId,
categoryId = categoryId,
userId = userId
)
}
fun toPostBody(postId: Int): PostBody {
return PostBody(
postId = postId,
textContent = textContent,
image1UUID = imageUUIDs.getOrNull(0),
image2UUID = imageUUIDs.getOrNull(1),
image3UUID = imageUUIDs.getOrNull(2)
)
}
}
data class CreatePostState(
val isLoading: Boolean = false,
val content: String = "",
val selectedCategoryId: Int? = null,
val selectedSectionId: Int = 0, // 默认分区0
val selectedImageUris: List<String> = emptyList(),
val uploadedImageUUIDs: List<String> = emptyList(),
val categories: List<Category> = emptyList(),
val isLoadingCategories: Boolean = false,
val isUploading: Boolean = false,
val uploadProgress: Map<String, Float> = emptyMap(),
val error: String? = null,
val success: Boolean = false
) {
val canPost: Boolean
get() = content.isNotBlank() &&
selectedCategoryId != null &&
!isLoading &&
!isUploading &&
uploadedImageUUIDs.size == selectedImageUris.size
val remainingImageSlots: Int
get() = maxOf(0, 3 - selectedImageUris.size)
val hasImages: Boolean
get() = selectedImageUris.isNotEmpty()
val isContentValid: Boolean
get() = content.isNotBlank() && content.length <= 2000
val contentCharCount: Int
get() = content.length
}

View File

@ -7,6 +7,8 @@ sealed class Screen(val route: String, val title: String) {
object Message : Screen("message", "消息")
object User : Screen("settings", "我的")
object About : Screen("about", "关于")
object DesignSystem : Screen("design_system", "设计系统")
object PostDetail : Screen("post_detail/{postId}", "帖子详情") {
fun createRoute(postId: Int) = "post_detail/$postId"
}

View File

@ -0,0 +1,66 @@
package com.qingshuige.tangyuan.repository
import com.qingshuige.tangyuan.api.ApiInterface
import com.qingshuige.tangyuan.model.Category
import com.qingshuige.tangyuan.model.CreatePostDto
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.awaitResponse
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class CreatePostRepository @Inject constructor(
private val apiInterface: ApiInterface
) {
/**
* 获取所有分类
*/
fun getAllCategories(): Flow<List<Category>> = flow {
try {
val response = apiInterface.getAllCategories().awaitResponse()
if (response.isSuccessful) {
response.body()?.let { emit(it) }
?: emit(emptyList())
} else {
throw Exception("Failed to get categories: ${response.message()}")
}
} catch (e: Exception) {
throw Exception("Network error: ${e.message}")
}
}
/**
* 创建新帖子
* 1. 先创建PostMetadata获取postId
* 2. 再创建PostBody
*/
suspend fun createPost(createPostDto: CreatePostDto, userId: Int): Result<Int> {
return try {
// 1. 创建PostMetadata
val metadataDto = createPostDto.toCreatPostMetadataDto(userId)
val metadataResponse = apiInterface.postPostMetadata(metadataDto).awaitResponse()
if (!metadataResponse.isSuccessful) {
return Result.failure(Exception("Failed to create post metadata: ${metadataResponse.message()}"))
}
val postId = metadataResponse.body()?.get("postId")
?: return Result.failure(Exception("No post ID returned"))
// 2. 创建PostBody
val postBody = createPostDto.toPostBody(postId)
val bodyResponse = apiInterface.postPostBody(postBody).awaitResponse()
if (!bodyResponse.isSuccessful) {
return Result.failure(Exception("Failed to create post body: ${bodyResponse.message()}"))
}
Result.success(postId)
} catch (e: Exception) {
Result.failure(Exception("Network error: ${e.message}"))
}
}
}

View File

@ -32,7 +32,6 @@ import com.qingshuige.tangyuan.TangyuanApplication
import com.qingshuige.tangyuan.model.CommentCard
import com.qingshuige.tangyuan.ui.theme.LiteraryFontFamily
import com.qingshuige.tangyuan.ui.theme.TangyuanGeneralFontFamily
import com.qingshuige.tangyuan.ui.theme.TangyuanShapes
import com.qingshuige.tangyuan.utils.withPanguSpacing
import java.util.Date
@ -123,6 +122,7 @@ private fun CommentMainContent(
lineHeight = 20.sp
),
fontFamily = LiteraryFontFamily,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface
)
@ -331,7 +331,7 @@ private fun CommentActionButton(
Text(
text = count.toString(),
style = MaterialTheme.typography.labelSmall,
fontFamily = LiteraryFontFamily,
fontFamily = TangyuanGeneralFontFamily,
color = color,
fontSize = 11.sp
)
@ -342,7 +342,7 @@ private fun CommentActionButton(
Text(
text = text,
style = MaterialTheme.typography.labelSmall,
fontFamily = LiteraryFontFamily,
fontFamily = TangyuanGeneralFontFamily,
color = color,
fontSize = 11.sp
)
@ -449,6 +449,7 @@ private fun ReplyItem(
lineHeight = 18.sp
),
fontFamily = LiteraryFontFamily,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface
)
@ -551,7 +552,9 @@ fun CommentInputBar(
text = if (replyToComment != null) "回复 ${replyToComment.authorName}" + ": " + {replyToComment.content.take(20) + if (replyToComment.content.length > 20) "..." else ""}()
else "说点什么...",
fontFamily = LiteraryFontFamily,
color = MaterialTheme.colorScheme.onSurfaceVariant
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(
alpha = 0.5f
)
)
},
shape = RoundedCornerShape(20.dp),

View File

@ -322,7 +322,7 @@ private fun PostCardContent(postCard: PostCard) {
lineHeight = 22.sp
),
fontFamily = LiteraryFontFamily,
fontWeight = FontWeight.Medium,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 6,
overflow = TextOverflow.Ellipsis

View File

@ -0,0 +1,591 @@
package com.qingshuige.tangyuan.ui.screens
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.qingshuige.tangyuan.ui.theme.*
import com.qingshuige.tangyuan.utils.withPanguSpacing
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
* 设计系统预览页面
*
* 用于集中展示和测试 `TangyuanTheme` 中的颜色排版和形状
* 方便设计师和开发者快速查阅和验证 UI 组件
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DesignSystemScreen(
onBackClick: () -> Unit = {}
) {
val scrollState = rememberScrollState()
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = "Tangyuan Design System",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
},
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "返回",
tint = MaterialTheme.colorScheme.onSurface
)
}
},
actions = {
IconButton(onClick = {}) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = "其他",
tint = MaterialTheme.colorScheme.onSurface
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
),
modifier = Modifier.fillMaxWidth()
)
}
) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.verticalScroll(scrollState)
.padding(16.dp)
) {
DesignTitleWithGuides()
// 颜色系统
DesignSection(title = "颜色系统 (Colors)") {
ColorSystemPreview()
}
// 排版系统
DesignSection(title = "排版系统 (Typography)") {
TypographySystemPreview()
}
// 形状系统
DesignSection(title = "形状系统 (Shapes)") {
ShapeSystemPreview()
}
}
}
}
}
@Composable
fun DesignTitleWithGuides() {
// 状态变量,用于存储测量到的标题和副标题的布局坐标
var titleCoords by remember { mutableStateOf<LayoutCoordinates?>(null) }
var subtitleCoords by remember { mutableStateOf<LayoutCoordinates?>(null) }
// 动画状态lineProgress 控制线条划过alpha 控制淡出
val lineProgress = remember { Animatable(0f) }
val alpha = remember { Animatable(1f) }
// 使用 LaunchedEffect 启动一次性动画
LaunchedEffect(Unit) {
// 启动一个协程来执行动画序列
launch {
// 1. 线条划入动画
lineProgress.animateTo(1f, animationSpec = tween(durationMillis = 600))
// 2. 短暂保持可见
delay(200)
// 3. 线条淡出动画
alpha.animateTo(0f, animationSpec = tween(durationMillis = 400))
}
}
// 定义参考线的颜色
val guideColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f)
Box(
modifier = Modifier.padding(bottom = 24.dp)
) {
// Canvas 用于在文本背后绘制引导线
Canvas(modifier = Modifier.matchParentSize()) {
val titleLayout = titleCoords
val subtitleLayout = subtitleCoords
// 确保坐标已经被测量到才开始绘制
if (titleLayout != null && subtitleLayout != null) {
val animatedAlphaColor = guideColor.copy(alpha = alpha.value)
// 计算整个标题区域的边界
val left = 0f
val top = 0f
val right = titleLayout.size.width.toFloat()
val bottom = subtitleLayout.positionInParent().y + subtitleLayout.size.height
// 动画进度
val progress = lineProgress.value
val dashEffect = PathEffect.dashPathEffect(floatArrayOf(15f, 15f), 0f)
// 绘制四条动态参考线
// 1. 从左到右的上边线
drawLine(animatedAlphaColor, start = Offset(left, top), end = Offset(right * progress, top), strokeWidth = 1.dp.toPx(), pathEffect = dashEffect)
// 2. 从左到右的下边线
drawLine(animatedAlphaColor, start = Offset(left, bottom), end = Offset(right * progress, bottom), strokeWidth = 1.dp.toPx(), pathEffect = dashEffect)
// 3. 从上到下的左边线
drawLine(animatedAlphaColor, start = Offset(left, top), end = Offset(left, bottom * progress), strokeWidth = 1.dp.toPx(), pathEffect = dashEffect)
// 4. 从上到下的右边线
drawLine(animatedAlphaColor, start = Offset(right, top), end = Offset(right, bottom * progress), strokeWidth = 1.dp.toPx(), pathEffect = dashEffect)
}
}
// 实际的标题和副标题文本
Column {
Text(
text = "糖原社区设计系统",
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.onGloballyPositioned { coordinates ->
titleCoords = coordinates
}
)
Text(
text = "现代简洁 · 文化雅致",
style = MaterialTheme.typography.titleMedium,
fontFamily = LiteraryFontFamily, // 保留文学字体
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.onGloballyPositioned { coordinates ->
subtitleCoords = coordinates
}
)
}
}
}
/**
* 设计系统的分区组件包含标题和内容
*/
@Composable
private fun DesignSection(title: String, content: @Composable () -> Unit) {
Column(modifier = Modifier.padding(vertical = 16.dp)) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 12.dp)
)
Surface(
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.surface,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(16.dp)) {
content()
}
}
}
}
/**
* 颜色系统预览
*/
@Composable
private fun ColorSystemPreview() {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("主题色 (Light / Dark)", style = MaterialTheme.typography.titleMedium)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround
) {
ColorRoleItem("Primary", TangyuanColors.PrimaryLight, TangyuanColors.PrimaryDark)
ColorRoleItem("Secondary", TangyuanColors.SecondaryLight, TangyuanColors.SecondaryDark)
ColorRoleItem("Tertiary", TangyuanColors.TertiaryLight, TangyuanColors.TertiaryDark)
ColorRoleItem("Accent", TangyuanColors.AccentLight, TangyuanColors.AccentDark)
}
Spacer(Modifier.height(8.dp))
Text("功能色 (Success / Warning / Error)", style = MaterialTheme.typography.titleMedium)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround
) {
ColorFunctionItem("Success", TangyuanColors.SuccessLight, TangyuanColors.SuccessDark)
ColorFunctionItem("Warning", TangyuanColors.WarningLight, TangyuanColors.WarningDark)
ColorFunctionItem("Error", TangyuanColors.ErrorLight, TangyuanColors.ErrorDark)
}
Spacer(Modifier.height(8.dp))
Text("界面基础色 (Background / Surface)", style = MaterialTheme.typography.titleMedium)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
SurfaceColorItem(
"Background",
TangyuanColors.BackgroundLight,
TangyuanColors.BackgroundDark,
TangyuanColors.OnBackgroundLight,
TangyuanColors.OnBackgroundDark
)
SurfaceColorItem(
"Surface",
TangyuanColors.SurfaceLight,
TangyuanColors.SurfaceDark,
TangyuanColors.OnSurfaceLight,
TangyuanColors.OnSurfaceDark
)
}
}
}
@Composable
private fun ColorRoleItem(name: String, lightColor: Color, darkColor: Color) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Row {
Box(
modifier = Modifier
.size(50.dp)
.background(lightColor)
)
Box(
modifier = Modifier
.size(50.dp)
.background(darkColor)
)
}
Text(name, style = MaterialTheme.typography.labelMedium)
}
}
@Composable
private fun RowScope.ColorFunctionItem(name: String, lightColor: Color, darkColor: Color) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.weight(1f)
) {
Box(
modifier = Modifier
.height(40.dp)
.fillMaxWidth()
.background(
brush = Brush.horizontalGradient(listOf(lightColor, darkColor)),
shape = MaterialTheme.shapes.small
)
.clip(MaterialTheme.shapes.small)
)
Text(
name,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 4.dp)
)
}
}
@Composable
private fun RowScope.SurfaceColorItem(
name: String,
lightBg: Color,
darkBg: Color,
lightContent: Color,
darkContent: Color
) {
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row {
Box(
modifier = Modifier
.size(60.dp)
.background(lightBg),
contentAlignment = Alignment.Center
) {
Text("Text", color = lightContent, style = MaterialTheme.typography.labelSmall)
}
Box(
modifier = Modifier
.size(60.dp)
.background(darkBg),
contentAlignment = Alignment.Center
) {
Text("Text", color = darkContent, style = MaterialTheme.typography.labelSmall)
}
}
Text(name, style = MaterialTheme.typography.labelMedium)
}
}
/**
* 排版系统预览
*/
@Composable
private fun TypographySystemPreview() {
val exampleText = "糖原社区 Tangyuan 2025"
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
// M3 Type Scale
TypographyItem("Headline Medium", exampleText, MaterialTheme.typography.headlineMedium)
TypographyItem("Title Large", exampleText, MaterialTheme.typography.titleLarge)
TypographyItem("Body Large", exampleText, MaterialTheme.typography.bodyLarge)
TypographyItem("Label Large", exampleText, MaterialTheme.typography.labelLarge)
// Font Weight Section
Spacer(Modifier.height(16.dp))
Text("字重 (Font Weights)", style = MaterialTheme.typography.titleMedium)
FontWeightShowcase()
// Chinese & Mixed Typography Section
Spacer(Modifier.height(16.dp))
Text("中英混排处理", style = MaterialTheme.typography.titleMedium)
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("自动盘古之白 (Pangu Spacing)", style = MaterialTheme.typography.titleSmall)
Text(
text = "在Tangyuan中使用Jetpack Compose构建UI。",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "在Tangyuan中使用Jetpack Compose构建UI。".withPanguSpacing(),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface
)
}
// Special Purpose Fonts
Spacer(Modifier.height(16.dp))
Text("特殊用途字体", style = MaterialTheme.typography.titleMedium)
// Literary Font
TypographyItem(
name = "文学字体 (Literary)",
exampleText = "人生若只如初见",
style = TextStyle(
fontFamily = LiteraryFontFamily,
fontWeight = FontWeight.SemiBold,
fontSize = 18.sp,
lineHeight = 28.sp,
letterSpacing = 0.8.sp
),
color = MaterialTheme.colorScheme.tertiary
)
// Other extended styles
TypographyItem(
"数字字体 (Number Large)",
"1,234,567",
TangyuanTypography.numberLarge,
MaterialTheme.colorScheme.primary
)
TypographyItem(
"代码字体 (Code)",
"val name = \"Tangyuan\"",
TangyuanTypography.code,
MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@Composable
private fun TypographyItem(
name: String,
exampleText: String,
style: TextStyle,
color: Color = MaterialTheme.colorScheme.onSurface
) {
Column(modifier = Modifier.padding(bottom = 8.dp)) {
Text(
text = name,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = exampleText,
style = style.copy(color = color),
maxLines = 1
)
Text(
text = "Font: ${getFontFamilyName(style.fontFamily)} | Size: ${style.fontSize.value.toInt()}sp | Weight: ${style.fontWeight?.weight}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@Composable
private fun FontWeightShowcase() {
val weights = listOf(
FontWeight.Normal to "Normal (400)",
FontWeight.Medium to "Medium (500)",
FontWeight.SemiBold to "SemiBold (600)",
FontWeight.Bold to "Bold (700)"
)
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
weights.forEach { (weight, name) ->
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = name,
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.width(120.dp)
)
Text(
text = "线粒体 XianlitiCN",
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = weight)
)
}
}
}
}
private fun getFontFamilyName(fontFamily: FontFamily?): String {
return when (fontFamily) {
TangyuanGeneralFontFamily -> "General (混排)"
EnglishFontFamily -> "Quicksand"
ChineseFontFamily -> "Noto Sans SC"
LiteraryFontFamily -> "Noto Serif SC (文学)"
FontFamily.Monospace -> "Monospace"
else -> "Default"
}
}
/**
* 形状系统预览
*/
@Composable
private fun ShapeSystemPreview() {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("Material Shapes", style = MaterialTheme.typography.titleMedium)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically
) {
ShapeItem("Extra Small (4dp)", MaterialTheme.shapes.extraSmall)
ShapeItem("Small (8dp)", MaterialTheme.shapes.small)
ShapeItem("Medium (12dp)", MaterialTheme.shapes.medium)
}
Spacer(Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically
) {
ShapeItem("Large (16dp)", MaterialTheme.shapes.large, Modifier.size(80.dp, 60.dp))
ShapeItem(
"Extra Large (28dp)",
MaterialTheme.shapes.extraLarge,
Modifier.size(80.dp, 60.dp)
)
}
Spacer(Modifier.height(16.dp))
Text("扩展形状 (TangyuanShapes)", style = MaterialTheme.typography.titleMedium)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically
) {
ShapeItem("Circle", TangyuanShapes.Circle)
ShapeItem("Top Rounded", TangyuanShapes.TopRounded)
ShapeItem("Cultural Card", TangyuanShapes.CulturalCard)
}
}
}
@Composable
private fun ShapeItem(name: String, shape: Shape, modifier: Modifier = Modifier.size(72.dp)) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box(
modifier = modifier
.background(MaterialTheme.colorScheme.primaryContainer, shape)
.border(1.dp, MaterialTheme.colorScheme.primary, shape)
)
Text(
text = name,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(top = 4.dp)
)
}
}
// ====================================
// 预览
// ====================================
@Preview(showBackground = true, name = "Design System - Light Theme")
@Composable
fun DesignSystemScreenLightPreview() {
TangyuanTheme(darkTheme = false) {
Surface {
DesignSystemScreen()
}
}
}
@Preview(showBackground = true, name = "Design System - Dark Theme")
@Composable
fun DesignSystemScreenDarkPreview() {
TangyuanTheme(darkTheme = true) {
Surface {
DesignSystemScreen()
}
}
}

View File

@ -137,8 +137,9 @@ private fun PostDetailTopBar(
TopAppBar(
title = {
Text(
text = "详情",
fontFamily = LiteraryFontFamily,
text = "帖子详情",
fontFamily = TangyuanGeneralFontFamily,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)
},
@ -309,6 +310,7 @@ private fun PostDetailCard(
lineHeight = 28.sp
),
fontFamily = LiteraryFontFamily,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface
)

View File

@ -44,6 +44,7 @@ fun UserDetailScreen(
userId: Int,
onBackClick: () -> Unit,
onPostClick: (Int) -> Unit = {},
onImageClick: (postId: Int, imageIndex: Int) -> Unit = { _, _ -> },
onFollowClick: () -> Unit = {},
sharedTransitionScope: SharedTransitionScope? = null,
animatedContentScope: AnimatedContentScope? = null,
@ -153,7 +154,7 @@ fun UserDetailScreen(
onBookmarkClick = { /* TODO: 实现收藏 */ },
onMoreClick = { /* TODO: 实现更多操作 */ },
onImageClick = { postId, imageIndex ->
// TODO: 实现图片点击
onImageClick(postId, imageIndex)
},
sharedTransitionScope = sharedTransitionScope,
animatedContentScope = animatedContentScope,
@ -189,14 +190,14 @@ private fun UserDetailTopBar(
if (isLoading) {
Text(
text = "用户详情",
style = MaterialTheme.typography.titleLarge,
style = MaterialTheme.typography.titleMedium,
fontFamily = TangyuanGeneralFontFamily,
color = MaterialTheme.colorScheme.onSurface
)
} else {
Text(
text = if (userName.isNotBlank()) "用户详情 · $userName" else "用户详情",
style = MaterialTheme.typography.titleLarge,
style = MaterialTheme.typography.titleMedium,
fontFamily = TangyuanGeneralFontFamily,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,

View File

@ -26,6 +26,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DesignServices
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.KeyboardArrowRight
@ -76,6 +77,7 @@ fun UserScreen(
onPostManagement: () -> Unit = {},
onSettings: () -> Unit = {},
onAbout: () -> Unit = {},
onDesignSystem: () -> Unit = {},
userViewModel: UserViewModel = hiltViewModel()
) {
val loginState by userViewModel.loginState.collectAsState()
@ -104,11 +106,18 @@ fun UserScreen(
MenuSection(
onPostManagement = onPostManagement,
onSettings = onSettings,
onAbout = onAbout
onAbout = onAbout,
onDesignSystem = onDesignSystem
)
} else {
// 未登录状态
NotLoggedInContent()
Spacer(modifier = Modifier.height(24.dp))
MenuSectionNotLogin(
onSettings = onSettings,
onAbout = onAbout,
onDesignSystem = onDesignSystem
)
}
}
}
@ -396,6 +405,7 @@ private fun VerticalDivider() {
@Composable
private fun MenuSection(
onPostManagement: () -> Unit,
onDesignSystem: () -> Unit,
onSettings: () -> Unit,
onAbout: () -> Unit
) {
@ -438,6 +448,76 @@ private fun MenuSection(
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)
)
MenuItem(
icon = Icons.Default.DesignServices,
title = "关于糖原设计系统",
subtitle = "了解 App 的设计系统与排版规范",
onClick = onDesignSystem,
showDivider = false
)
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)
)
MenuItem(
icon = Icons.Default.Info,
title = "关于",
subtitle = "版本信息和帮助",
onClick = onAbout,
showDivider = false
)
}
}
}
@Composable
private fun MenuSectionNotLogin(
onSettings: () -> Unit,
onDesignSystem: () -> Unit,
onAbout: () -> Unit
) {
Text(
text = "功能菜单",
style = MaterialTheme.typography.titleMedium,
fontFamily = TangyuanGeneralFontFamily,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)
)
Box(
modifier = Modifier.fillMaxWidth(),
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
MenuItem(
icon = Icons.Default.Settings,
title = "设置",
subtitle = "个性化设置和隐私选项",
onClick = onSettings
)
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)
)
MenuItem(
icon = Icons.Default.DesignServices,
title = "关于糖原设计系统",
subtitle = "了解 App 的设计系统与排版规范",
onClick = onDesignSystem,
showDivider = false
)
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)
)
MenuItem(
icon = Icons.Default.Info,
title = "关于",