feat: 复刻小程序四大主页面与底部导航(UI 优先,本地 mock)
This commit is contained in:
@@ -13,4 +13,6 @@ dependencies {
|
||||
implementation(project(":core:network"))
|
||||
implementation(project(":core:database"))
|
||||
implementation(project(":core:datastore"))
|
||||
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.mingyuan.android_client.core.data.di
|
||||
|
||||
import com.mingyuan.android_client.core.data.repository.FakeCheckinRepository
|
||||
import com.mingyuan.android_client.core.data.repository.FakeHomeRepository
|
||||
import com.mingyuan.android_client.core.data.repository.FakePlanRepository
|
||||
import com.mingyuan.android_client.core.data.repository.FakeProfileRepository
|
||||
import com.mingyuan.android_client.core.domain.repository.CheckinRepository
|
||||
import com.mingyuan.android_client.core.domain.repository.HomeRepository
|
||||
import com.mingyuan.android_client.core.domain.repository.PlanRepository
|
||||
import com.mingyuan.android_client.core.domain.repository.ProfileRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class DataModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindHomeRepository(impl: FakeHomeRepository): HomeRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindCheckinRepository(impl: FakeCheckinRepository): CheckinRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindPlanRepository(impl: FakePlanRepository): PlanRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindProfileRepository(impl: FakeProfileRepository): ProfileRepository
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.mingyuan.android_client.core.data.repository
|
||||
|
||||
import com.mingyuan.android_client.core.domain.repository.CheckinRepository
|
||||
import com.mingyuan.android_client.core.model.CheckinRecord
|
||||
import com.mingyuan.android_client.core.model.CheckinStatus
|
||||
import com.mingyuan.android_client.core.model.GroupMember
|
||||
import com.mingyuan.android_client.core.model.PersonalCheckinRecord
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class FakeCheckinRepository @Inject constructor() : CheckinRepository {
|
||||
|
||||
private val record = MutableStateFlow(seedCheckin())
|
||||
|
||||
override fun observeCheckin(): Flow<CheckinRecord> = flow {
|
||||
delay(250)
|
||||
emitAll(record)
|
||||
}
|
||||
|
||||
override suspend fun submitCheckin(status: CheckinStatus) {
|
||||
delay(300)
|
||||
record.update { current ->
|
||||
current.copy(
|
||||
checked = true,
|
||||
status = status,
|
||||
groupMembers = current.groupMembers.map { member ->
|
||||
if (member.id == "me") {
|
||||
member.copy(checkedIn = true, checkinTime = "现在", status = status)
|
||||
} else {
|
||||
member
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun seedCheckin() = CheckinRecord(
|
||||
classDate = "2026-09-09",
|
||||
weekday = "周三",
|
||||
dayNum = 9,
|
||||
checked = false,
|
||||
status = null,
|
||||
groupName = "进阶一组",
|
||||
groupMembers = listOf(
|
||||
GroupMember("m1", "张三", true, "19:02", CheckinStatus.NORMAL),
|
||||
GroupMember("m2", "李四", true, "19:30", CheckinStatus.LATE),
|
||||
GroupMember("me", "我", false, null, null),
|
||||
GroupMember("m3", "王五", true, "09:10", CheckinStatus.LEAVE),
|
||||
GroupMember("m4", "赵六", false, null, null),
|
||||
),
|
||||
personalHistory = listOf(
|
||||
PersonalCheckinRecord("2026-09-06", CheckinStatus.NORMAL, "19:05"),
|
||||
PersonalCheckinRecord("2026-09-02", CheckinStatus.LATE, "19:32"),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.mingyuan.android_client.core.data.repository
|
||||
|
||||
import com.mingyuan.android_client.core.domain.repository.HomeRepository
|
||||
import com.mingyuan.android_client.core.model.HomeData
|
||||
import com.mingyuan.android_client.core.model.HomeModule
|
||||
import com.mingyuan.android_client.core.model.IntroCard
|
||||
import com.mingyuan.android_client.core.model.ModuleDestination
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class FakeHomeRepository @Inject constructor() : HomeRepository {
|
||||
override fun observeHome(): Flow<HomeData> = flow {
|
||||
delay(250)
|
||||
emit(seedHome())
|
||||
}
|
||||
|
||||
private fun seedHome() = HomeData(
|
||||
greeting = "晚上好",
|
||||
displayName = "张三",
|
||||
encouragement = "坚持每周打卡,与小组共同进步。",
|
||||
modules = listOf(
|
||||
HomeModule("checkin", "📋", "打卡介绍", "每日打卡记录", ModuleDestination.CHECKIN),
|
||||
HomeModule("plan", "📝", "计划介绍", "周/月/年计划", ModuleDestination.PLAN),
|
||||
HomeModule("about", "ℹ️", "关于我们", "了解明缘云道场", ModuleDestination.ABOUT),
|
||||
),
|
||||
intros = listOf(
|
||||
IntroCard(
|
||||
"📚 学习成长工具",
|
||||
"明缘云道场是一款专注于个人成长的效率工具,通过每周打卡与计划管理,帮助你养成良好的学习习惯,实现持续进步。好好学习,天天向上。",
|
||||
),
|
||||
IntroCard(
|
||||
"✅ 周打卡",
|
||||
"每周打卡,支持多种状态记录(正常、迟到、请假、早退、不参加讨论等)。查看小组打卡情况,与同学们互相监督、共同进步。",
|
||||
),
|
||||
IntroCard(
|
||||
"📝 计划管理",
|
||||
"提供周计划、月计划、年计划三级管理,支持文字与图片上传。清晰追踪每个周期的目标完成情况,让成长有迹可循。",
|
||||
),
|
||||
IntroCard(
|
||||
"👥 小组共学",
|
||||
"加入学习小组,查看组员的打卡与计划完成情况。在集体的氛围中保持学习动力,与志同道合的伙伴一起成长。",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.mingyuan.android_client.core.data.repository
|
||||
|
||||
import com.mingyuan.android_client.core.domain.repository.PlanRepository
|
||||
import com.mingyuan.android_client.core.model.MyPlanRecord
|
||||
import com.mingyuan.android_client.core.model.PlanGroup
|
||||
import com.mingyuan.android_client.core.model.PlanMember
|
||||
import com.mingyuan.android_client.core.model.PlanRecord
|
||||
import com.mingyuan.android_client.core.model.PlanSubmitData
|
||||
import com.mingyuan.android_client.core.model.PlanType
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class FakePlanRepository @Inject constructor() : PlanRepository {
|
||||
|
||||
private val plans = PlanType.entries.associateWith { type -> MutableStateFlow(seedPlan(type)) }
|
||||
private val submits = mutableMapOf<String, MutableStateFlow<PlanSubmitData>>()
|
||||
|
||||
override fun observePlan(type: PlanType): Flow<PlanRecord> = flow {
|
||||
delay(250)
|
||||
emitAll(plans.getValue(type))
|
||||
}
|
||||
|
||||
override fun observePlanSubmit(type: PlanType, periodKey: String): Flow<PlanSubmitData> = flow {
|
||||
delay(250)
|
||||
emitAll(submitState(type, periodKey))
|
||||
}
|
||||
|
||||
override suspend fun savePlan(
|
||||
type: PlanType,
|
||||
periodKey: String,
|
||||
content: String,
|
||||
attachments: List<String>,
|
||||
) {
|
||||
delay(300)
|
||||
val submit = submitState(type, periodKey)
|
||||
submit.value = submit.value.copy(content = content, attachments = attachments, checked = true)
|
||||
val plan = plans.getValue(type)
|
||||
plan.value = plan.value.copy(
|
||||
myChecked = true,
|
||||
myRecord = MyPlanRecord("已完成", "现在", content, attachments),
|
||||
)
|
||||
}
|
||||
|
||||
private fun submitState(type: PlanType, periodKey: String): MutableStateFlow<PlanSubmitData> =
|
||||
submits.getOrPut("${type.name}:$periodKey") { MutableStateFlow(seedSubmit(type)) }
|
||||
|
||||
private fun seedPlan(type: PlanType) = PlanRecord(
|
||||
type = type,
|
||||
periodKey = periodKeyFor(type),
|
||||
canSubmit = true,
|
||||
message = if (type == PlanType.WEEKLY) "本周计划提交截止:周日晚 23:59" else null,
|
||||
myChecked = type == PlanType.WEEKLY,
|
||||
myRecord = if (type == PlanType.WEEKLY) {
|
||||
MyPlanRecord("已完成", "今天 18:30", "本周完成两章学习,整理笔记并复习错题。", listOf("图1", "图2"))
|
||||
} else {
|
||||
null
|
||||
},
|
||||
groups = listOf(
|
||||
PlanGroup("g1", "进阶一组", 4, 6, memberSeed()),
|
||||
PlanGroup("g2", "进阶二组", 5, 5, memberSeed()),
|
||||
PlanGroup("g3", "进阶三组", 2, 7, memberSeed()),
|
||||
),
|
||||
unassignedCount = 2,
|
||||
)
|
||||
|
||||
private fun seedSubmit(type: PlanType) = PlanSubmitData(
|
||||
content = if (type == PlanType.WEEKLY) "本周完成两章学习,整理笔记并复习错题。" else "",
|
||||
attachments = if (type == PlanType.WEEKLY) listOf("图1", "图2") else emptyList(),
|
||||
checked = type == PlanType.WEEKLY,
|
||||
canSubmit = true,
|
||||
message = null,
|
||||
history = if (type == PlanType.WEEKLY) {
|
||||
emptyList()
|
||||
} else {
|
||||
listOf(MyPlanRecord("已完成", "2026-08 完成", "上月计划内容……", emptyList()))
|
||||
},
|
||||
)
|
||||
|
||||
private fun periodKeyFor(type: PlanType): String = when (type) {
|
||||
PlanType.WEEKLY -> "2026 第 37 周"
|
||||
PlanType.MONTHLY -> "2026 年 9 月"
|
||||
PlanType.YEARLY -> "2026 年"
|
||||
}
|
||||
|
||||
private fun memberSeed() = listOf(
|
||||
PlanMember("p1", "张三", true),
|
||||
PlanMember("p2", "李四", true),
|
||||
PlanMember("p3", "王五", false),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.mingyuan.android_client.core.data.repository
|
||||
|
||||
import com.mingyuan.android_client.core.domain.repository.ProfileRepository
|
||||
import com.mingyuan.android_client.core.model.ClassGroup
|
||||
import com.mingyuan.android_client.core.model.UserProfile
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class FakeProfileRepository @Inject constructor() : ProfileRepository {
|
||||
|
||||
private val profile = MutableStateFlow(seedProfile())
|
||||
|
||||
override fun observeProfile(): Flow<UserProfile> = flow {
|
||||
delay(250)
|
||||
emitAll(profile)
|
||||
}
|
||||
|
||||
override suspend fun deleteAccount() {
|
||||
delay(300)
|
||||
profile.value = seedProfile().copy(
|
||||
displayName = "游客",
|
||||
name = "",
|
||||
studentId = "",
|
||||
phone = null,
|
||||
group = null,
|
||||
avatarUrl = null,
|
||||
classGroups = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun seedProfile() = UserProfile(
|
||||
displayName = "张三",
|
||||
name = "张三",
|
||||
studentId = "20260001",
|
||||
phone = "138****1234",
|
||||
role = "成员",
|
||||
group = "进阶一组",
|
||||
loginType = "手机号",
|
||||
avatarUrl = null,
|
||||
classGroups = listOf(
|
||||
ClassGroup("2026 秋季", "进阶一组", "成员"),
|
||||
ClassGroup("2026 春季", "进阶二组", "组长"),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.mingyuan.android_client.core.designsystem.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mingyuan.android_client.core.designsystem.theme.AmberGradient
|
||||
import com.mingyuan.android_client.core.designsystem.theme.Divider
|
||||
|
||||
@Composable
|
||||
fun AmberProgressBar(
|
||||
progress: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val fraction = progress.coerceIn(0f, 1f)
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(8.dp)
|
||||
.background(Divider, RoundedCornerShape(4.dp)),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fraction)
|
||||
.height(8.dp)
|
||||
.background(AmberGradient, RoundedCornerShape(4.dp)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.mingyuan.android_client.core.designsystem.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mingyuan.android_client.core.designsystem.theme.BrandAmber
|
||||
|
||||
@Composable
|
||||
fun Avatar(
|
||||
initial: String,
|
||||
modifier: Modifier = Modifier,
|
||||
size: Dp = 40.dp,
|
||||
backgroundColor: Color = BrandAmber,
|
||||
textColor: Color = Color.White,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.size(size).background(backgroundColor, CircleShape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = initial.take(1),
|
||||
color = textColor,
|
||||
fontSize = (size.value * 0.42f).sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.mingyuan.android_client.core.designsystem.component
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mingyuan.android_client.core.designsystem.theme.MutedText
|
||||
import com.mingyuan.android_client.core.designsystem.theme.PrimaryText
|
||||
|
||||
@Composable
|
||||
fun LabeledRow(
|
||||
label: String,
|
||||
value: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||
) {
|
||||
Text(label, color = MutedText, fontSize = 13.sp)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Text(
|
||||
text = value,
|
||||
color = PrimaryText,
|
||||
fontSize = 14.sp,
|
||||
textAlign = TextAlign.End,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.mingyuan.android_client.core.designsystem.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mingyuan.android_client.core.designsystem.theme.AmberGradient
|
||||
import com.mingyuan.android_client.core.designsystem.theme.BrandAmber
|
||||
import com.mingyuan.android_client.core.designsystem.theme.Danger
|
||||
import com.mingyuan.android_client.core.designsystem.theme.Disabled
|
||||
|
||||
enum class ButtonVariant { PRIMARY, DANGER, OUTLINE }
|
||||
|
||||
@Composable
|
||||
fun MingyuanButton(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
variant: ButtonVariant = ButtonVariant.PRIMARY,
|
||||
enabled: Boolean = true,
|
||||
fullWidth: Boolean = false,
|
||||
) {
|
||||
val shape = RoundedCornerShape(50)
|
||||
|
||||
val backgroundModifier = when {
|
||||
!enabled -> Modifier.background(Disabled, shape)
|
||||
variant == ButtonVariant.PRIMARY -> Modifier.background(AmberGradient, shape)
|
||||
else -> Modifier.background(Color.White, shape)
|
||||
}
|
||||
|
||||
val borderModifier = when {
|
||||
!enabled -> Modifier
|
||||
variant == ButtonVariant.DANGER -> Modifier.border(1.dp, Danger, shape)
|
||||
variant == ButtonVariant.OUTLINE -> Modifier.border(1.dp, BrandAmber, shape)
|
||||
else -> Modifier
|
||||
}
|
||||
|
||||
val textColor = when {
|
||||
!enabled -> Color.White
|
||||
variant == ButtonVariant.PRIMARY -> Color.White
|
||||
variant == ButtonVariant.DANGER -> Danger
|
||||
else -> BrandAmber
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.then(if (fullWidth) Modifier.fillMaxWidth() else Modifier)
|
||||
.clip(shape)
|
||||
.then(backgroundModifier)
|
||||
.then(borderModifier)
|
||||
.clickable(enabled = enabled, onClick = onClick)
|
||||
.padding(horizontal = 24.dp, vertical = 12.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = textColor,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.mingyuan.android_client.core.designsystem.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mingyuan.android_client.core.designsystem.theme.SoftShadow
|
||||
|
||||
@Composable
|
||||
fun SectionCard(
|
||||
modifier: Modifier = Modifier,
|
||||
cornerRadius: Int = 16,
|
||||
backgroundColor: Color = Color.White,
|
||||
contentPadding: Int = 16,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.shadow(
|
||||
elevation = 4.dp,
|
||||
shape = RoundedCornerShape(cornerRadius.dp),
|
||||
ambientColor = SoftShadow,
|
||||
spotColor = SoftShadow,
|
||||
)
|
||||
.background(backgroundColor, RoundedCornerShape(cornerRadius.dp))
|
||||
.padding(contentPadding.dp),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.mingyuan.android_client.core.designsystem.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mingyuan.android_client.core.designsystem.theme.BrandAmber
|
||||
import com.mingyuan.android_client.core.designsystem.theme.SecondaryText
|
||||
import com.mingyuan.android_client.core.designsystem.theme.SegmentedTrack
|
||||
|
||||
@Composable
|
||||
fun SegmentedControl(
|
||||
labels: List<String>,
|
||||
selectedIndex: Int,
|
||||
onSelect: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(SegmentedTrack, RoundedCornerShape(50))
|
||||
.padding(4.dp),
|
||||
) {
|
||||
labels.forEachIndexed { index, label ->
|
||||
val selected = index == selectedIndex
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(50))
|
||||
.background(if (selected) Color.White else Color.Transparent)
|
||||
.clickable { onSelect(index) }
|
||||
.padding(vertical = 8.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
color = if (selected) BrandAmber else SecondaryText,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.mingyuan.android_client.core.designsystem.component
|
||||
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mingyuan.android_client.core.designsystem.theme.SkeletonColor
|
||||
|
||||
@Composable
|
||||
fun SkeletonBox(
|
||||
modifier: Modifier = Modifier,
|
||||
cornerRadius: Int = 12,
|
||||
) {
|
||||
val transition = rememberInfiniteTransition(label = "skeleton")
|
||||
val alpha by transition.animateFloat(
|
||||
initialValue = 0.4f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 700, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
),
|
||||
label = "skeleton-alpha",
|
||||
)
|
||||
Box(
|
||||
modifier = modifier
|
||||
.graphicsLayer { this.alpha = alpha }
|
||||
.background(SkeletonColor, RoundedCornerShape(cornerRadius.dp)),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.mingyuan.android_client.core.designsystem.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mingyuan.android_client.core.designsystem.theme.Danger
|
||||
import com.mingyuan.android_client.core.designsystem.theme.ErrorBackground
|
||||
import com.mingyuan.android_client.core.designsystem.theme.FaintText
|
||||
import com.mingyuan.android_client.core.designsystem.theme.MutedText
|
||||
import com.mingyuan.android_client.core.designsystem.theme.NeutralChip
|
||||
|
||||
@Composable
|
||||
fun EmptyState(
|
||||
emoji: String,
|
||||
message: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth().padding(vertical = 32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.size(72.dp).background(NeutralChip, CircleShape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(emoji, fontSize = 32.sp)
|
||||
}
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(message, color = FaintText, fontSize = 14.sp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorState(
|
||||
message: String,
|
||||
onRetry: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth().padding(vertical = 32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.size(72.dp).background(ErrorBackground, CircleShape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text("!", color = Danger, fontSize = 32.sp, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(message, color = MutedText, fontSize = 14.sp)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
MingyuanButton(text = "重新加载", onClick = onRetry)
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,48 @@ package com.mingyuan.android_client.core.designsystem.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
// Brand palette — ported from the WeChat mini-program design tokens.
|
||||
val PageBackground = Color(0xFFFDFBF7)
|
||||
val PlanBackground = Color(0xFFF6F2EC)
|
||||
|
||||
val Purple40 = Color(0xFF6650A4)
|
||||
val PurpleGrey40 = Color(0xFF625B71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
val PrimaryText = Color(0xFF2C1810)
|
||||
val SecondaryText = Color(0xFF5A4A3A)
|
||||
val MutedText = Color(0xFF8B7A6A)
|
||||
val MutedLightText = Color(0xFFA99A89)
|
||||
val FaintText = Color(0xFFB6A996)
|
||||
|
||||
val BrandAmber = Color(0xFFC67B30)
|
||||
val LightAmber = Color(0xFFE3B37C)
|
||||
val AmberMid = Color(0xFFE4A05E)
|
||||
val AmberBright = Color(0xFFEFA45D)
|
||||
|
||||
val HeroGradientStart = Color(0xFF2C1810)
|
||||
val HeroGradientMid = Color(0xFF4A2B1D)
|
||||
val HeroGradientEnd = Color(0xFF6B3F24)
|
||||
|
||||
val StatusNormal = Color(0xFF2E7D32)
|
||||
val StatusLate = Color(0xFFC67B30)
|
||||
val StatusLeave = Color(0xFF3A7BD5)
|
||||
val StatusEarlyLeave = Color(0xFF8B7A6A)
|
||||
val StatusAbsent = Color(0xFFC0392B)
|
||||
|
||||
val Danger = Color(0xFFC0392B)
|
||||
val ErrorBackground = Color(0xFFFDEAEA)
|
||||
val SuccessBackground = Color(0xFFE8F4EB)
|
||||
val WarningBackground = Color(0xFFFFF3E6)
|
||||
val WarningBorder = Color(0xFFF7D9B9)
|
||||
|
||||
val NeutralChip = Color(0xFFF6EFE6)
|
||||
val SegmentedTrack = Color(0xFFEFE7DC)
|
||||
val Divider = Color(0xFFF2EDE7)
|
||||
val PillIdleBorder = Color(0xFFD8C7B4)
|
||||
val TextareaBorder = Color(0xFFE4D9CB)
|
||||
val Disabled = Color(0xFFD8C7B4)
|
||||
|
||||
val TabBackground = Color(0xFFFFFFFF)
|
||||
val TabInactive = Color(0xFF5A4A3A)
|
||||
val TabActive = Color(0xFFC67B30)
|
||||
|
||||
val SoftShadow = Color(0x0D2C1810)
|
||||
val SkeletonColor = Color(0xFFEFE6D9)
|
||||
val SkeletonHighlight = Color(0xFFFAF4EC)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.mingyuan.android_client.core.designsystem.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
|
||||
// Brand gradients (135deg, top-left → bottom-right, matching the mini-program).
|
||||
val HeroGradient = Brush.linearGradient(
|
||||
colors = listOf(HeroGradientStart, HeroGradientMid, HeroGradientEnd),
|
||||
)
|
||||
|
||||
val AmberGradient = Brush.linearGradient(
|
||||
colors = listOf(BrandAmber, AmberMid),
|
||||
)
|
||||
|
||||
val AvatarGradient = Brush.linearGradient(
|
||||
colors = listOf(BrandAmber, LightAmber),
|
||||
)
|
||||
@@ -1,49 +1,59 @@
|
||||
package com.mingyuan.android_client.core.designsystem.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80,
|
||||
)
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40,
|
||||
primary = BrandAmber,
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = LightAmber,
|
||||
onPrimaryContainer = PrimaryText,
|
||||
secondary = AmberMid,
|
||||
onSecondary = Color.White,
|
||||
tertiary = StatusLeave,
|
||||
background = PageBackground,
|
||||
onBackground = PrimaryText,
|
||||
surface = Color.White,
|
||||
onSurface = PrimaryText,
|
||||
surfaceVariant = NeutralChip,
|
||||
onSurfaceVariant = MutedText,
|
||||
error = Danger,
|
||||
onError = Color.White,
|
||||
outline = Divider,
|
||||
outlineVariant = PillIdleBorder,
|
||||
)
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = LightAmber,
|
||||
onPrimary = PrimaryText,
|
||||
primaryContainer = HeroGradientMid,
|
||||
onPrimaryContainer = LightAmber,
|
||||
background = HeroGradientStart,
|
||||
onBackground = Color(0xFFFDFBF7),
|
||||
surface = Color(0xFF2A1E18),
|
||||
onSurface = Color(0xFFFDFBF7),
|
||||
surfaceVariant = Color(0xFF3A2A20),
|
||||
onSurfaceVariant = MutedLightText,
|
||||
error = Color(0xFFE5736C),
|
||||
)
|
||||
|
||||
/**
|
||||
* The app's single Material theme. All UI entry points wrap their content in [MingyuanTheme].
|
||||
*
|
||||
* Dynamic color is deliberately disabled so the brand palette always renders as designed,
|
||||
* regardless of the device's wallpaper-based Material You scheme.
|
||||
*/
|
||||
@Composable
|
||||
fun MingyuanTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+.
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme,
|
||||
typography = Typography,
|
||||
content = content,
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:model"))
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.truth)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.mingyuan.android_client.core.domain.repository
|
||||
|
||||
import com.mingyuan.android_client.core.model.CheckinRecord
|
||||
import com.mingyuan.android_client.core.model.CheckinStatus
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface CheckinRepository {
|
||||
fun observeCheckin(): Flow<CheckinRecord>
|
||||
|
||||
suspend fun submitCheckin(status: CheckinStatus)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.mingyuan.android_client.core.domain.repository
|
||||
|
||||
import com.mingyuan.android_client.core.model.HomeData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface HomeRepository {
|
||||
fun observeHome(): Flow<HomeData>
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.mingyuan.android_client.core.domain.repository
|
||||
|
||||
import com.mingyuan.android_client.core.model.PlanRecord
|
||||
import com.mingyuan.android_client.core.model.PlanSubmitData
|
||||
import com.mingyuan.android_client.core.model.PlanType
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface PlanRepository {
|
||||
fun observePlan(type: PlanType): Flow<PlanRecord>
|
||||
|
||||
fun observePlanSubmit(type: PlanType, periodKey: String): Flow<PlanSubmitData>
|
||||
|
||||
suspend fun savePlan(
|
||||
type: PlanType,
|
||||
periodKey: String,
|
||||
content: String,
|
||||
attachments: List<String>,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.mingyuan.android_client.core.domain.repository
|
||||
|
||||
import com.mingyuan.android_client.core.model.UserProfile
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ProfileRepository {
|
||||
fun observeProfile(): Flow<UserProfile>
|
||||
|
||||
suspend fun deleteAccount()
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.mingyuan.android_client.core.model
|
||||
|
||||
enum class CheckinStatus(val label: String) {
|
||||
NORMAL("正常"),
|
||||
LATE("迟到"),
|
||||
LEAVE("请假"),
|
||||
ABSENT("不参加讨论"),
|
||||
EARLY_LEAVE("早退"),
|
||||
}
|
||||
|
||||
data class GroupMember(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val checkedIn: Boolean,
|
||||
val checkinTime: String?,
|
||||
val status: CheckinStatus?,
|
||||
)
|
||||
|
||||
data class PersonalCheckinRecord(
|
||||
val date: String,
|
||||
val status: CheckinStatus,
|
||||
val time: String,
|
||||
)
|
||||
|
||||
data class CheckinRecord(
|
||||
val classDate: String,
|
||||
val weekday: String,
|
||||
val dayNum: Int,
|
||||
val checked: Boolean,
|
||||
val status: CheckinStatus?,
|
||||
val groupName: String,
|
||||
val groupMembers: List<GroupMember>,
|
||||
val personalHistory: List<PersonalCheckinRecord>,
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.mingyuan.android_client.core.model
|
||||
|
||||
enum class ModuleDestination {
|
||||
CHECKIN,
|
||||
PLAN,
|
||||
ABOUT,
|
||||
}
|
||||
|
||||
data class HomeModule(
|
||||
val id: String,
|
||||
val emoji: String,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val destination: ModuleDestination,
|
||||
)
|
||||
|
||||
data class IntroCard(
|
||||
val title: String,
|
||||
val body: String,
|
||||
)
|
||||
|
||||
data class HomeData(
|
||||
val greeting: String,
|
||||
val displayName: String,
|
||||
val encouragement: String,
|
||||
val modules: List<HomeModule>,
|
||||
val intros: List<IntroCard>,
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.mingyuan.android_client.core.model
|
||||
|
||||
enum class PlanType(val label: String) {
|
||||
WEEKLY("周计划"),
|
||||
MONTHLY("月计划"),
|
||||
YEARLY("年计划"),
|
||||
}
|
||||
|
||||
data class MyPlanRecord(
|
||||
val status: String,
|
||||
val submittedTime: String,
|
||||
val content: String,
|
||||
val attachments: List<String>,
|
||||
)
|
||||
|
||||
data class PlanMember(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val filled: Boolean,
|
||||
)
|
||||
|
||||
data class PlanGroup(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val checkedCount: Int,
|
||||
val memberCount: Int,
|
||||
val members: List<PlanMember>,
|
||||
)
|
||||
|
||||
data class PlanRecord(
|
||||
val type: PlanType,
|
||||
val periodKey: String,
|
||||
val canSubmit: Boolean,
|
||||
val message: String?,
|
||||
val myChecked: Boolean,
|
||||
val myRecord: MyPlanRecord?,
|
||||
val groups: List<PlanGroup>,
|
||||
val unassignedCount: Int,
|
||||
)
|
||||
|
||||
data class PlanSubmitData(
|
||||
val content: String,
|
||||
val attachments: List<String>,
|
||||
val checked: Boolean,
|
||||
val canSubmit: Boolean,
|
||||
val message: String?,
|
||||
val history: List<MyPlanRecord>,
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.mingyuan.android_client.core.model
|
||||
|
||||
data class ClassGroup(
|
||||
val period: String,
|
||||
val group: String,
|
||||
val role: String,
|
||||
)
|
||||
|
||||
data class UserProfile(
|
||||
val displayName: String,
|
||||
val name: String,
|
||||
val studentId: String,
|
||||
val phone: String?,
|
||||
val role: String,
|
||||
val group: String?,
|
||||
val loginType: String,
|
||||
val avatarUrl: String?,
|
||||
val classGroups: List<ClassGroup>,
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.mingyuan.android_client.core.ui.status
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mingyuan.android_client.core.model.CheckinStatus
|
||||
|
||||
@Composable
|
||||
fun StatusBadge(
|
||||
status: CheckinStatus,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val color = status.statusColor()
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(color.copy(alpha = 0.12f), RoundedCornerShape(50))
|
||||
.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(Modifier.size(6.dp).background(color, CircleShape))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(status.label, color = color, fontSize = 12.sp, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.mingyuan.android_client.core.ui.status
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.mingyuan.android_client.core.designsystem.theme.StatusAbsent
|
||||
import com.mingyuan.android_client.core.designsystem.theme.StatusEarlyLeave
|
||||
import com.mingyuan.android_client.core.designsystem.theme.StatusLate
|
||||
import com.mingyuan.android_client.core.designsystem.theme.StatusLeave
|
||||
import com.mingyuan.android_client.core.designsystem.theme.StatusNormal
|
||||
import com.mingyuan.android_client.core.model.CheckinStatus
|
||||
|
||||
fun CheckinStatus.statusColor(): Color = when (this) {
|
||||
CheckinStatus.NORMAL -> StatusNormal
|
||||
CheckinStatus.LATE -> StatusLate
|
||||
CheckinStatus.LEAVE -> StatusLeave
|
||||
CheckinStatus.ABSENT -> StatusAbsent
|
||||
CheckinStatus.EARLY_LEAVE -> StatusEarlyLeave
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.mingyuan.android_client.core.ui.status
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mingyuan.android_client.core.designsystem.theme.PillIdleBorder
|
||||
import com.mingyuan.android_client.core.designsystem.theme.SecondaryText
|
||||
import com.mingyuan.android_client.core.model.CheckinStatus
|
||||
|
||||
@Composable
|
||||
fun StatusPill(
|
||||
status: CheckinStatus,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val color = status.statusColor()
|
||||
val shape = RoundedCornerShape(50)
|
||||
|
||||
val background = if (selected) color else Color.White
|
||||
val textColor = if (selected) Color.White else SecondaryText
|
||||
val borderColor = if (selected) color else PillIdleBorder
|
||||
val dotColor = if (selected) Color.White else color
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.shadow(
|
||||
elevation = if (selected) 6.dp else 0.dp,
|
||||
shape = shape,
|
||||
spotColor = color.copy(alpha = 0.22f),
|
||||
ambientColor = color.copy(alpha = 0.22f),
|
||||
)
|
||||
.background(background, shape)
|
||||
.border(1.dp, borderColor, shape)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 14.dp, vertical = 9.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(Modifier.size(8.dp).background(dotColor, CircleShape))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(status.label, color = textColor, fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user