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:
parent
c5ec5b1a0b
commit
a528b623b9
2
.idea/deploymentTargetSelector.xml
generated
2
.idea/deploymentTargetSelector.xml
generated
@ -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
13
.idea/deviceManager.xml
generated
Normal 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>
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 = "关于",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user