feat: 复刻小程序四大主页面与底部导航(UI 优先,本地 mock)

This commit is contained in:
AndyChen1024
2026-09-09 19:57:47 +08:00
parent f240682281
commit c80fb31eb1
53 changed files with 2778 additions and 68 deletions

View File

@@ -13,4 +13,6 @@ dependencies {
implementation(project(":core:network"))
implementation(project(":core:database"))
implementation(project(":core:datastore"))
implementation(libs.kotlinx.coroutines.core)
}

View File

@@ -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
}

View File

@@ -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"),
),
)
}

View File

@@ -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(
"👥 小组共学",
"加入学习小组,查看组员的打卡与计划完成情况。在集体的氛围中保持学习动力,与志同道合的伙伴一起成长。",
),
),
)
}

View File

@@ -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),
)
}

View File

@@ -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 春季", "进阶二组", "组长"),
),
)
}