feat: 新增 Fake 仓库实现(含可提交的打卡/计划与会话)
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import Combine
|
||||
import CoreDomain
|
||||
import CoreModel
|
||||
|
||||
/// Fake 打卡仓库(骨架阶段返回假数据,可提交)。
|
||||
public final class FakeCheckinRepository: CheckinRepository, @unchecked Sendable {
|
||||
private let record: CurrentValueSubject<CheckinRecord, Never>
|
||||
|
||||
public init() {
|
||||
record = CurrentValueSubject(Self.seed())
|
||||
}
|
||||
|
||||
public func observeCheckin() -> AsyncThrowingStream<CheckinRecord, Error> {
|
||||
Self.stream(record)
|
||||
}
|
||||
|
||||
public func submitCheckin(status: CheckinStatus) async throws {
|
||||
try await Task.sleep(for: .milliseconds(300))
|
||||
var current = record.value
|
||||
current = CheckinRecord(
|
||||
classDate: current.classDate,
|
||||
weekday: current.weekday,
|
||||
dayNum: current.dayNum,
|
||||
checked: true,
|
||||
status: status,
|
||||
groupName: current.groupName,
|
||||
groupMembers: current.groupMembers.map { member in
|
||||
guard member.id == "me" else { return member }
|
||||
return GroupMember(id: member.id, name: member.name, checkedIn: true, checkinTime: "现在", status: status)
|
||||
},
|
||||
personalHistory: current.personalHistory
|
||||
)
|
||||
record.value = current
|
||||
}
|
||||
|
||||
private static func stream<T>(_ subject: CurrentValueSubject<T, Never>) -> AsyncThrowingStream<T, Error> {
|
||||
AsyncThrowingStream { continuation in
|
||||
let cancellable = subject.sink { continuation.yield($0) }
|
||||
continuation.onTermination = { _ in cancellable.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
private static func seed() -> CheckinRecord {
|
||||
CheckinRecord(
|
||||
classDate: "2026-09-09",
|
||||
weekday: "周三",
|
||||
dayNum: 9,
|
||||
checked: false,
|
||||
status: nil,
|
||||
groupName: "进阶一组",
|
||||
groupMembers: [
|
||||
GroupMember(id: "m1", name: "张三", checkedIn: true, checkinTime: "19:02", status: .normal),
|
||||
GroupMember(id: "m2", name: "李四", checkedIn: true, checkinTime: "19:30", status: .late),
|
||||
GroupMember(id: "me", name: "我", checkedIn: false, checkinTime: nil, status: nil),
|
||||
GroupMember(id: "m3", name: "王五", checkedIn: true, checkinTime: "09:10", status: .leave),
|
||||
GroupMember(id: "m4", name: "赵六", checkedIn: false, checkinTime: nil, status: nil),
|
||||
],
|
||||
personalHistory: [
|
||||
PersonalCheckinRecord(date: "2026-09-06", status: .normal, time: "19:05"),
|
||||
PersonalCheckinRecord(date: "2026-09-02", status: .late, time: "19:32"),
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,19 @@ import CoreCommon
|
||||
import CoreDomain
|
||||
import CoreModel
|
||||
|
||||
/// Fake 仓库实现(骨架阶段返回假数据,后续替换为 OfflineFirst 实现)。
|
||||
/// Fake 首页仓库(骨架阶段返回假数据,后续替换为 OfflineFirst 实现)。
|
||||
public struct FakeHomeRepository: HomeRepository {
|
||||
public init() {}
|
||||
|
||||
public func observeHome() -> AsyncThrowingStream<HomeData, Error> {
|
||||
stream(Self.loggedInData)
|
||||
}
|
||||
|
||||
public func observeGuestHome() -> AsyncThrowingStream<HomeData, Error> {
|
||||
stream(Self.guestData)
|
||||
}
|
||||
|
||||
private func stream(_ data: HomeData) -> AsyncThrowingStream<HomeData, Error> {
|
||||
AsyncThrowingStream { continuation in
|
||||
let task = Task {
|
||||
do {
|
||||
@@ -14,7 +22,7 @@ public struct FakeHomeRepository: HomeRepository {
|
||||
try await Task.sleep(for: .milliseconds(400))
|
||||
try Task.checkCancellation()
|
||||
Logger.info("FakeHomeRepository 返回假数据")
|
||||
continuation.yield(Self.fakeData)
|
||||
continuation.yield(data)
|
||||
continuation.finish()
|
||||
} catch {
|
||||
continuation.finish(throwing: error)
|
||||
@@ -26,15 +34,32 @@ public struct FakeHomeRepository: HomeRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private static let fakeData = HomeData(
|
||||
id: "home-1",
|
||||
greeting: "早上好,Andy",
|
||||
summary: "今日已完成 2 / 4",
|
||||
habits: [
|
||||
Habit(id: "habit-1", title: "晨跑 5 公里", detail: "保持运动习惯", isCheckedInToday: true, streakDays: 12),
|
||||
Habit(id: "habit-2", title: "阅读 30 分钟", detail: "每天进步一点", isCheckedInToday: true, streakDays: 8),
|
||||
Habit(id: "habit-3", title: "喝水 8 杯", detail: "健康补水", isCheckedInToday: false, streakDays: 0),
|
||||
Habit(id: "habit-4", title: "写日记", detail: "记录今日所思", isCheckedInToday: false, streakDays: 3),
|
||||
]
|
||||
private static let modules = [
|
||||
HomeModule(id: "checkin", emoji: "📋", title: "打卡介绍", description: "每日打卡记录", destination: .checkin),
|
||||
HomeModule(id: "plan", emoji: "📝", title: "计划介绍", description: "周/月/年计划", destination: .plan),
|
||||
HomeModule(id: "about", emoji: "ℹ️", title: "关于我们", description: "了解明缘云道场", destination: .about),
|
||||
]
|
||||
|
||||
private static let intros = [
|
||||
IntroCard(title: "📚 学习成长工具", body: "明缘云道场是一款专注于个人成长的效率工具,通过每周打卡与计划管理,帮助你养成良好的学习习惯,实现持续进步。好好学习,天天向上。"),
|
||||
IntroCard(title: "✅ 周打卡", body: "每周打卡,支持多种状态记录(正常、迟到、请假、早退、不参加讨论等)。查看小组打卡情况,与同学们互相监督、共同进步。"),
|
||||
IntroCard(title: "📝 计划管理", body: "提供周计划、月计划、年计划三级管理,支持文字与图片上传。清晰追踪每个周期的目标完成情况,让成长有迹可循。"),
|
||||
IntroCard(title: "👥 小组共学", body: "加入学习小组,查看组员的打卡与计划完成情况。在集体的氛围中保持学习动力,与志同道合的伙伴一起成长。"),
|
||||
]
|
||||
|
||||
private static let guestData = HomeData(
|
||||
greeting: "欢迎来到明缘云道场",
|
||||
displayName: "游客",
|
||||
encouragement: "登录后即可打卡、提交计划",
|
||||
modules: modules,
|
||||
intros: intros
|
||||
)
|
||||
|
||||
private static let loggedInData = HomeData(
|
||||
greeting: "晚上好",
|
||||
displayName: "张三",
|
||||
encouragement: "坚持每周打卡,与小组共同进步。",
|
||||
modules: modules,
|
||||
intros: intros
|
||||
)
|
||||
}
|
||||
|
||||
136
IosClientKit/Sources/CoreRepository/FakePlanRepository.swift
Normal file
136
IosClientKit/Sources/CoreRepository/FakePlanRepository.swift
Normal file
@@ -0,0 +1,136 @@
|
||||
import Combine
|
||||
import CoreDomain
|
||||
import CoreModel
|
||||
import Foundation
|
||||
|
||||
/// Fake 计划仓库(骨架阶段返回假数据,可提交)。
|
||||
public final class FakePlanRepository: PlanRepository, @unchecked Sendable {
|
||||
private let plans: [PlanType: CurrentValueSubject<PlanRecord, Never>]
|
||||
private var submits: [String: CurrentValueSubject<PlanSubmitData, Never>] = [:]
|
||||
private let lock = NSLock()
|
||||
|
||||
public init() {
|
||||
var dict: [PlanType: CurrentValueSubject<PlanRecord, Never>] = [:]
|
||||
for type in PlanType.allCases {
|
||||
dict[type] = CurrentValueSubject<PlanRecord, Never>(Self.seedPlan(type))
|
||||
}
|
||||
plans = dict
|
||||
}
|
||||
|
||||
public func observePlan(type: PlanType) -> AsyncThrowingStream<PlanRecord, Error> {
|
||||
Self.stream(plans[type]!)
|
||||
}
|
||||
|
||||
public func observePlanSubmit(type: PlanType, periodKey: String) -> AsyncThrowingStream<PlanSubmitData, Error> {
|
||||
Self.stream(submitSubject(type: type, periodKey: periodKey))
|
||||
}
|
||||
|
||||
public func savePlan(type: PlanType, periodKey: String, content: String, attachments: [String]) async throws {
|
||||
try await Task.sleep(for: .milliseconds(300))
|
||||
|
||||
let submit = submitSubject(type: type, periodKey: periodKey)
|
||||
let currentSubmit = submit.value
|
||||
submit.value = PlanSubmitData(
|
||||
content: content,
|
||||
attachments: attachments,
|
||||
checked: true,
|
||||
canSubmit: currentSubmit.canSubmit,
|
||||
message: currentSubmit.message,
|
||||
history: currentSubmit.history
|
||||
)
|
||||
|
||||
let plan = plans[type]!
|
||||
let currentPlan = plan.value
|
||||
plan.value = PlanRecord(
|
||||
type: currentPlan.type,
|
||||
periodKey: currentPlan.periodKey,
|
||||
canSubmit: currentPlan.canSubmit,
|
||||
message: currentPlan.message,
|
||||
myChecked: true,
|
||||
myRecord: MyPlanRecord(status: "已完成", submittedTime: "现在", content: content, attachments: attachments),
|
||||
groups: currentPlan.groups,
|
||||
unassignedCount: currentPlan.unassignedCount
|
||||
)
|
||||
}
|
||||
|
||||
private func submitSubject(type: PlanType, periodKey: String) -> CurrentValueSubject<PlanSubmitData, Never> {
|
||||
let key = "\(type.rawValue):\(periodKey)"
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
if let existing = submits[key] {
|
||||
return existing
|
||||
}
|
||||
let subject = CurrentValueSubject<PlanSubmitData, Never>(Self.seedSubmit(type: type))
|
||||
submits[key] = subject
|
||||
return subject
|
||||
}
|
||||
|
||||
private static func stream<T>(_ subject: CurrentValueSubject<T, Never>) -> AsyncThrowingStream<T, Error> {
|
||||
AsyncThrowingStream { continuation in
|
||||
let cancellable = subject.sink { continuation.yield($0) }
|
||||
continuation.onTermination = { _ in cancellable.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
private static func seedPlan(_ type: PlanType) -> PlanRecord {
|
||||
PlanRecord(
|
||||
type: type,
|
||||
periodKey: periodKey(for: type),
|
||||
canSubmit: true,
|
||||
message: type == .weekly ? "本周计划提交截止:周日晚 23:59" : nil,
|
||||
myChecked: type == .weekly,
|
||||
myRecord: type == .weekly
|
||||
? MyPlanRecord(status: "已完成", submittedTime: "今天 18:30", content: "本周完成两章学习,整理笔记并复习错题。", attachments: ["图1", "图2"])
|
||||
: nil,
|
||||
groups: seedGroups(),
|
||||
unassignedCount: 2
|
||||
)
|
||||
}
|
||||
|
||||
private static func seedSubmit(type: PlanType) -> PlanSubmitData {
|
||||
if type == .weekly {
|
||||
return PlanSubmitData(
|
||||
content: "本周完成两章学习,整理笔记并复习错题。",
|
||||
attachments: ["图1", "图2"],
|
||||
checked: true,
|
||||
canSubmit: true,
|
||||
message: nil,
|
||||
history: []
|
||||
)
|
||||
}
|
||||
return PlanSubmitData(
|
||||
content: "",
|
||||
attachments: [],
|
||||
checked: false,
|
||||
canSubmit: true,
|
||||
message: nil,
|
||||
history: [
|
||||
MyPlanRecord(status: "已完成", submittedTime: "2026-08 完成", content: "上月计划内容……", attachments: []),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
private static func periodKey(for type: PlanType) -> String {
|
||||
switch type {
|
||||
case .weekly: return "2026 第 37 周"
|
||||
case .monthly: return "2026 年 9 月"
|
||||
case .yearly: return "2026 年"
|
||||
}
|
||||
}
|
||||
|
||||
private static func seedGroups() -> [PlanGroup] {
|
||||
[
|
||||
PlanGroup(id: "g1", name: "进阶一组", checkedCount: 4, memberCount: 6, members: seedMembers()),
|
||||
PlanGroup(id: "g2", name: "进阶二组", checkedCount: 5, memberCount: 5, members: seedMembers()),
|
||||
PlanGroup(id: "g3", name: "进阶三组", checkedCount: 2, memberCount: 7, members: seedMembers()),
|
||||
]
|
||||
}
|
||||
|
||||
private static func seedMembers() -> [PlanMember] {
|
||||
[
|
||||
PlanMember(id: "p1", name: "张三", filled: true),
|
||||
PlanMember(id: "p2", name: "李四", filled: true),
|
||||
PlanMember(id: "p3", name: "王五", filled: false),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import CoreCommon
|
||||
import CoreDomain
|
||||
import CoreModel
|
||||
|
||||
/// Fake 个人资料仓库(骨架阶段返回假数据)。
|
||||
public struct FakeProfileRepository: ProfileRepository {
|
||||
public init() {}
|
||||
|
||||
public func observeProfile() -> AsyncThrowingStream<UserProfile, Error> {
|
||||
AsyncThrowingStream { continuation in
|
||||
let task = Task {
|
||||
do {
|
||||
try await Task.sleep(for: .milliseconds(400))
|
||||
try Task.checkCancellation()
|
||||
Logger.info("FakeProfileRepository 返回假数据")
|
||||
continuation.yield(Self.fakeData)
|
||||
continuation.finish()
|
||||
} catch {
|
||||
continuation.finish(throwing: error)
|
||||
}
|
||||
}
|
||||
continuation.onTermination = { _ in task.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
private static let fakeData = UserProfile(
|
||||
displayName: "张三",
|
||||
name: "张三",
|
||||
studentId: "20260001",
|
||||
phone: "138****1234",
|
||||
role: "成员",
|
||||
group: "进阶一组",
|
||||
loginType: "手机号",
|
||||
avatarUrl: nil,
|
||||
classGroups: [
|
||||
ClassGroup(period: "2026 秋季", group: "进阶一组", role: "成员"),
|
||||
ClassGroup(period: "2026 春季", group: "进阶二组", role: "组长"),
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import Combine
|
||||
import CoreDomain
|
||||
import Foundation
|
||||
|
||||
/// Fake 会话仓库(登录态在进程内共享,默认未登录)。
|
||||
public final class FakeSessionRepository: SessionRepository, @unchecked Sendable {
|
||||
private let isLoggedIn = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
public init() {}
|
||||
|
||||
public func observeIsLoggedIn() -> AsyncThrowingStream<Bool, Error> {
|
||||
AsyncThrowingStream { continuation in
|
||||
let cancellable = self.isLoggedIn.sink { continuation.yield($0) }
|
||||
continuation.onTermination = { _ in cancellable.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
public func requestSmsCode(phone: String) async throws {
|
||||
try await Task.sleep(for: .milliseconds(500))
|
||||
guard phone.range(of: #"^1\d{10}$"#, options: .regularExpression) != nil else {
|
||||
throw SessionError.invalidPhone
|
||||
}
|
||||
}
|
||||
|
||||
public func login(phone: String, code: String) async throws {
|
||||
try await Task.sleep(for: .milliseconds(500))
|
||||
guard code == "123456" else {
|
||||
throw SessionError.wrongCode
|
||||
}
|
||||
guard phone == "13812341234" else {
|
||||
throw SessionError.phoneNotMatched
|
||||
}
|
||||
isLoggedIn.value = true
|
||||
}
|
||||
|
||||
public func logout() async throws {
|
||||
isLoggedIn.value = false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user