From c39a3245349b01a462992edb23e165b5d8f0248a Mon Sep 17 00:00:00 2001 From: AndyChen1024 Date: Tue, 15 Sep 2026 14:35:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20Fake=20=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E5=AE=9E=E7=8E=B0=EF=BC=88=E5=90=AB=E5=8F=AF=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E7=9A=84=E6=89=93=E5=8D=A1/=E8=AE=A1=E5=88=92?= =?UTF-8?q?=E4=B8=8E=E4=BC=9A=E8=AF=9D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FakeCheckinRepository.swift | 64 +++++++++ .../CoreRepository/FakeHomeRepository.swift | 49 +++++-- .../CoreRepository/FakePlanRepository.swift | 136 ++++++++++++++++++ .../FakeProfileRepository.swift | 40 ++++++ .../FakeSessionRepository.swift | 39 +++++ 5 files changed, 316 insertions(+), 12 deletions(-) create mode 100644 IosClientKit/Sources/CoreRepository/FakeCheckinRepository.swift create mode 100644 IosClientKit/Sources/CoreRepository/FakePlanRepository.swift create mode 100644 IosClientKit/Sources/CoreRepository/FakeProfileRepository.swift create mode 100644 IosClientKit/Sources/CoreRepository/FakeSessionRepository.swift diff --git a/IosClientKit/Sources/CoreRepository/FakeCheckinRepository.swift b/IosClientKit/Sources/CoreRepository/FakeCheckinRepository.swift new file mode 100644 index 0000000..718a815 --- /dev/null +++ b/IosClientKit/Sources/CoreRepository/FakeCheckinRepository.swift @@ -0,0 +1,64 @@ +import Combine +import CoreDomain +import CoreModel + +/// Fake 打卡仓库(骨架阶段返回假数据,可提交)。 +public final class FakeCheckinRepository: CheckinRepository, @unchecked Sendable { + private let record: CurrentValueSubject + + public init() { + record = CurrentValueSubject(Self.seed()) + } + + public func observeCheckin() -> AsyncThrowingStream { + 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(_ subject: CurrentValueSubject) -> AsyncThrowingStream { + 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"), + ] + ) + } +} diff --git a/IosClientKit/Sources/CoreRepository/FakeHomeRepository.swift b/IosClientKit/Sources/CoreRepository/FakeHomeRepository.swift index b822687..f022eec 100644 --- a/IosClientKit/Sources/CoreRepository/FakeHomeRepository.swift +++ b/IosClientKit/Sources/CoreRepository/FakeHomeRepository.swift @@ -2,11 +2,19 @@ import CoreCommon import CoreDomain import CoreModel -/// Fake 仓库实现(骨架阶段返回假数据,后续替换为 OfflineFirst 实现)。 +/// Fake 首页仓库(骨架阶段返回假数据,后续替换为 OfflineFirst 实现)。 public struct FakeHomeRepository: HomeRepository { public init() {} public func observeHome() -> AsyncThrowingStream { + stream(Self.loggedInData) + } + + public func observeGuestHome() -> AsyncThrowingStream { + stream(Self.guestData) + } + + private func stream(_ data: HomeData) -> AsyncThrowingStream { 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 ) } diff --git a/IosClientKit/Sources/CoreRepository/FakePlanRepository.swift b/IosClientKit/Sources/CoreRepository/FakePlanRepository.swift new file mode 100644 index 0000000..a2a429f --- /dev/null +++ b/IosClientKit/Sources/CoreRepository/FakePlanRepository.swift @@ -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] + private var submits: [String: CurrentValueSubject] = [:] + private let lock = NSLock() + + public init() { + var dict: [PlanType: CurrentValueSubject] = [:] + for type in PlanType.allCases { + dict[type] = CurrentValueSubject(Self.seedPlan(type)) + } + plans = dict + } + + public func observePlan(type: PlanType) -> AsyncThrowingStream { + Self.stream(plans[type]!) + } + + public func observePlanSubmit(type: PlanType, periodKey: String) -> AsyncThrowingStream { + 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 { + let key = "\(type.rawValue):\(periodKey)" + lock.lock() + defer { lock.unlock() } + if let existing = submits[key] { + return existing + } + let subject = CurrentValueSubject(Self.seedSubmit(type: type)) + submits[key] = subject + return subject + } + + private static func stream(_ subject: CurrentValueSubject) -> AsyncThrowingStream { + 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), + ] + } +} diff --git a/IosClientKit/Sources/CoreRepository/FakeProfileRepository.swift b/IosClientKit/Sources/CoreRepository/FakeProfileRepository.swift new file mode 100644 index 0000000..0d65c53 --- /dev/null +++ b/IosClientKit/Sources/CoreRepository/FakeProfileRepository.swift @@ -0,0 +1,40 @@ +import CoreCommon +import CoreDomain +import CoreModel + +/// Fake 个人资料仓库(骨架阶段返回假数据)。 +public struct FakeProfileRepository: ProfileRepository { + public init() {} + + public func observeProfile() -> AsyncThrowingStream { + 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: "组长"), + ] + ) +} diff --git a/IosClientKit/Sources/CoreRepository/FakeSessionRepository.swift b/IosClientKit/Sources/CoreRepository/FakeSessionRepository.swift new file mode 100644 index 0000000..d42eeb9 --- /dev/null +++ b/IosClientKit/Sources/CoreRepository/FakeSessionRepository.swift @@ -0,0 +1,39 @@ +import Combine +import CoreDomain +import Foundation + +/// Fake 会话仓库(登录态在进程内共享,默认未登录)。 +public final class FakeSessionRepository: SessionRepository, @unchecked Sendable { + private let isLoggedIn = CurrentValueSubject(false) + + public init() {} + + public func observeIsLoggedIn() -> AsyncThrowingStream { + 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 + } +}