Compare commits

...

2 Commits

Author SHA1 Message Date
wendazhi 3b6cb52149 线上配置1
Production deploy / build (push) Successful in 1m29s Details
2026-02-12 17:57:24 +08:00
wendazhi 48dbd4b49f 线上配置 2026-02-12 17:56:34 +08:00
42 changed files with 828 additions and 2845 deletions

View File

@ -27,7 +27,7 @@ jobs:
rm -rf repo rm -rf repo
git clone "${REPO_URL}" repo git clone "${REPO_URL}" repo
- name: Copy backend to /data/wildgrowth/weizhuozhongzhi-ai - name: Copy backend to /data/wildgrowth/backend
run: | run: |
set -e set -e
cd repo cd repo

View File

@ -27,7 +27,7 @@ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no "$SERVER" << EOF
echo "✅ 已更新 APPLE_SHARED_SECRET" echo "✅ 已更新 APPLE_SHARED_SECRET"
else else
echo "APPLE_SHARED_SECRET=$SECRET" >> .env echo "APPLE_SHARED_SECRET=$SECRET" >> .env
echo "✅ 已添加 APPLE_SHARED_SECRET" echo "✅ 已添加 "
fi fi
# 验证配置 # 验证配置

View File

@ -1,3 +1,21 @@
#后端启动端口号配置
PORT=3005
#数据库配置
DATABASE_URL=postgresql://wildgrowth_rw:olCRbB9EKrMEfb@localhost:5432/wildgrowth?schema=public DATABASE_URL=postgresql://wildgrowth_rw:olCRbB9EKrMEfb@localhost:5432/wildgrowth?schema=public
JWT_SECRET=IZLHw83LLhlmeia2HjolCRbB9EKrMEfb
PORT=3005 # 文件配置
MAX_FILE_SIZE=2097152
# 登录配置
JWT_SECRET=wild-growth-dev-secret-key-change-in-production
JWT_EXPIRES_IN=7d
# 豆包 API
DOUBAO_API_KEY=a3e13a85-437f-448c-aaa9-14292cd5e0ab
# 阿里云号码认证
ALIYUN_ACCESS_KEY_ID=LTAI5tQRdJ76RKA8Eoz2kWG4
ALIYUN_ACCESS_KEY_SECRET=ymgGigzo3OA10wIapNdCwxjbK6zdVn
ALIYUN_PHONE_VERIFY_SIGN_NAME=速通互联验证码
ALIYUN_PHONE_VERIFY_TEMPLATE_CODE=100001

View File

@ -1,50 +0,0 @@
# 「Logic Fixed」版 CompletionView 审查报告(禁止应用)
**审查对象**Gemini 提供的 Cyber Polaroid - Logic Fixed 版(内部持有 UserManager、点击拉取后端、参数仅 courseId/courseTitle/path
**结论**:仅审查,**不应用、不修改**仓库内任何文件。
---
## 一、逻辑层与需求符合性
| 项目 | 要求 | 本版实现 | 结论 |
|------|------|----------|------|
| **点击按钮时拉后端** | 用户点击后,在 CompletionView 内部调接口拉最新数据 | `fetchAndDevelop()``try await userManager.fetchUserProfile()`,再显影展示 | ✅ 符合 |
| **数据权责在 CompletionView** | 不依赖父视图传入 completedLessonCount / focusMinutes | 仅入参courseId, courseTitle, navigationPath展示用 `userManager.studyStats.lessons`、`userManager.studyStats.time` | ✅ 符合 |
| **回到我的内容** | 必须调用 navStore.switchToGrowthTab() | `handleBackToContent()` 内先 `navStore.switchToGrowthTab()`,再清 path 或 dismiss | ✅ 符合 |
| **前端持久化动画状态** | 已显影过则直接展示结果,不重复播动画 | UserDefaults `has_revealed_course_\(courseId)`onAppear 时 `checkDevelopmentStatus()` | ✅ 符合 |
---
## 二、接口与调用方影响
| 项目 | 说明 |
|------|------|
| **CompletionView 入参** | courseId, courseTitle, navigationPath? — 与当前仓库中 VerticalScreenPlayerView 的调用一致,**无新增必选参数**。 |
| **VerticalScreenPlayerView** | 无需修改,现有 `CompletionView(courseId:, courseTitle:, navigationPath:)` 可直接编译。 |
| **其他页面** | 无其他调用处,逻辑与展示不受影响。 |
结论:**仅替换 CompletionView.swift 即可,无需改其他文件。**
---
## 三、实现细节核对
| 项目 | 说明 |
|------|------|
| **UserManager.studyStats** | 仓库中为 `(time: Int, lessons: Int)`,本版使用 `userManager.studyStats.lessons`、`userManager.studyStats.time`,字段一致。 |
| **拉取失败时** | `fetchUserProfile()` 抛错时 catch 仅 print仍执行 `MainActor.run { isDeveloped = true; ... }`,即失败也显影并展示当前 userManager 数据,按钮不会一直 Loading行为合理。 |
| **0.8s 延时** | 显影前 `Task.sleep(0.8s)` 为体验延时,与「先拉接口再显影」不冲突(实际应在 fetch 完成后显影当前顺序为sleep → fetch → 显影)。若希望「拉完再显影、无固定延时」,可去掉 sleep 或改为仅在实际请求完成后显影。 |
---
## 四、审查结论汇总
| 项目 | 结论 |
|------|------|
| **逻辑层** | 点击拉后端、数据来自 UserManager、不新增父视图业务参数符合「不改逻辑层」的约定。 |
| **接口** | 仅 courseId / courseTitle / navigationPath与现有一致调用方零改动。 |
| **其他页面** | 不受影响。 |
| **建议** | 逻辑与接口均可接受;若采用,仅全量替换 CompletionView.swift。0.8s 延时可视产品需求保留或去掉。 |
**未对仓库内任何文件进行修改。**

View File

@ -1,96 +0,0 @@
# CompletionView Magic Card 版 — 审查报告(不应用)
**审查日期**2025-01-29
**范围**Magic Card Edition粒子庆祝 + 按钮卡片一体化 + 极光光晕)
**结论**:仅审查、不修改仓库;逻辑继承正确,**ParticleModifier 存在两处需修复的实现问题**。
---
## 1. 需求落实情况
| 项目 | 要求 | 定稿实现 | 结论 |
|------|------|----------|------|
| **按钮卡片一体化** | 按钮在卡片内,点击后「献祭」化作彩带和数据 | IdleActionView 中按钮,切换后显示 ActiveDashboard + ConfettiExplosion | ✅ |
| **粒子爆炸** | 蓝/紫/粉色粒子炸开 | ConfettiExplosionCircle + CapsuleconfettiColors | ✅ |
| **呼吸光晕** | 激活后卡片背景极淡极光渐变 | AngularGradient + blur(20)isSystemOn 时显示 | ✅ |
| **极简流程** | 点击上传 → 按钮消失 → 数据呈现,无「已同步」占位 | 符合 | ✅ |
| **色彩规范** | 按钮 System Blue | brandBlue = Color.blue | ✅ |
---
## 2. 逻辑继承
| 项目 | 结论 |
|------|------|
| **持久化 Key** | ✅ `has_revealed_course_\(courseId)` |
| **游客短路** | ✅ 0.5s 延迟,`performActivation(0, 0)`,不调网络 |
| **登录用户** | ✅ 0.8s 延迟 → fetchUserProfile → performActivation |
| **checkSystemStatus** | ✅ 已激活时恢复最终值 |
| **数字滚动** | ✅ `withAnimation(.linear(1.5))` + RollingNumberText |
| **ScaleButtonStyle** | 未使用,无重复定义风险 |
---
## 3. ⚠️ ParticleModifier 需修复项
### 3.1 角度单位错误(影响粒子方向)
**问题**`angle` 为角度制 (0~360°),但 `cos(angle)`、`sin(angle)` 在 Swift 中接收弧度制。未做转换会导致粒子方向错误。
**修复**:先转换为弧度再计算:
```swift
let rad = angle * .pi / 180
offset(x: time == 0 ? 0 : cos(rad) * distance,
y: time == 0 ? 0 : sin(rad) * distance)
```
### 3.2 angle / distance 每次重算导致动画抖动
**问题**`var angle: Double { Double.random(in: 0...360) }` 和 `var distance: Double { ... }` 为计算属性,`body` 每次求值都会得到新随机数,粒子目标位置不断变化,动画会抖动。
**修复**:在 `init` 中一次性随机,并用 `@State` 保存,例如:
```swift
let index: Int
@State private var time: Double = 0.0
private let angle: Double
private let distance: Double
init(index: Int) {
self.index = index
self.angle = Double.random(in: 0..<360)
self.distance = Double.random(in: 80...150)
// 如需在 init 中初始化 @State,可用 _time = State(initialValue: 0)
}
```
并在 `body` 中用 `angle`、`distance` 计算 offset配合上述弧度转换
---
## 4. ConfettiExplosion 可选优化
- **`confettiColors.randomElement()!`**:在 `body` 中每次重绘都会重新随机,粒子颜色可能闪变。可按 `index` 固定颜色,如 `confettiColors[i % confettiColors.count]`,以保持每个粒子颜色稳定。
- **ForEach(0..<15)**:若 Swift 版本要求,可为 `ForEach` 增加 `id: \.self`
---
## 5. 接口与影响范围
| 项目 | 结论 |
|------|------|
| **初始化参数** | 三参数不变 |
| **替换方式** | 整文件替换 `CompletionView.swift` |
| **新增类型** | `ParticleModifier` 仅在本文件使用,无符号冲突 |
| **RollingNumberText** | 同前,保留在本文件 |
---
## 6. 总结
| 维度 | 结论 |
|------|------|
| **需求落实** | ✅ 卡片一体化、粒子、光晕、流程、色彩均符合 |
| **逻辑继承** | ✅ Key、游客、登录用户、数字滚动正确 |
| **ParticleModifier** | ⚠️ **需修复**:弧度转换 + angle/distance 稳定化 |
| **接口兼容** | ✅ 可直接替换 |
**审查结论** Magic Card 版在设计与逻辑上正确,应用前需修复 ParticleModifier 的角度单位与随机值稳定性,否则粒子动画会出现方向错误和抖动。**本次未对仓库做任何修改。**

View File

@ -1,133 +0,0 @@
# 完成页导航方案 — 审查报告
**审查对象**:基于「零副作用 / SSOT / 响应式导航 / 原生手感」的完成页与播放器导航改造方案
**审查结论**:只做审查,不修改代码;本报告供决策与后续实现参考。
---
## 一、方案目标与标准(引用)
| 标准 | 含义 |
|-----|------|
| **零副作用** | 不引入「假页面」污染数据源,无 Hack |
| **单一事实来源 (SSOT)** | 数据源 `allCourseNodes` 仅含真实课程章节 |
| **响应式导航** | 用 `NavigationPath` 堆栈精确控制流向,消除白屏回退 |
| **原生手感** | 用手势识别(非页面索引)触发跳转 |
---
## 二、方案要点摘要
1. **删除 CompletionPlaceholderPage 的导航职责**(或整页删除,视实现而定)
- 若保留:仅作纯视觉占位,无 `onAppear` 等导航逻辑。
2. **VerticalScreenPlayerView**
- 在**最后一节**挂载「边缘左滑手势」修饰符(如 `LastPageSwipeModifier`)。
- 左滑触发:`navigationPath.append(CourseNavigation.completion(...))`,或先设 `currentNodeId = "wg://completion"` 再由现有 `onChange` 统一 push。
- TabView 仅渲染真实 `allCourseNodes`,不把「完成」当作一节数据。
3. **CompletionView**
- 新增 `@Binding var navigationPath: NavigationPath`
- 「回到我的内容」/「继续学习」调用 `handleReturnToMap()``navigationPath.removeLast(2)`,一次 pop 完成页 + 播放器,直接回地图。
4. **调用方**
- GrowthView / ProfileView / DiscoveryView 的 `.completion` 分支向 `CompletionView` 传入对应 path 的 `Binding`(如 `$navStore.growthPath` 等)。
---
## 三、与当前实现的对照
| 维度 | 当前实现 | 方案 |
|------|----------|------|
| **数据源** | `allCourseNodes` 纯净TabView 多一页 `CompletionPlaceholderPage` 用 tag `"wg://completion"`**未**写入 `allCourseNodes` | 与当前一致或更进一步:完全移除占位页,仅手势触发 push |
| **进入完成页** | 左滑到占位页 → `onChange(of: currentNodeId)` → 0.1s 后 append `.completion` | 最后一节左滑手势 → 直接 append 或先切 tag 再 append不依赖「多一页」 |
| **完成页返回** | `navStore.switchToGrowthTab()` + `dismiss()`,先回播放器再靠系统/逻辑 | `removeLast(2)` 穿透回地图,无中间层 |
| **占位页** | 存在,纯视觉 + 父视图 `onChange` 驱动 push无占位内 `onAppear` | 方案 A保留为纯视觉方案 B删除仅手势 |
**结论**:方案在「单一事实来源」和「响应式导航」上比当前更彻底;当前已避免在数据源里掺假节点,但仍依赖 TabView 多一页占位。
---
## 四、按标准的符合度
### 4.1 零副作用
- **方案**:若不保留占位页,则无「假页」;若保留占位页且仅视觉、无逻辑,则也算零逻辑副作用。
- **当前**:占位页无 `onAppear` 导航,副作用已收敛,但 TabView 仍多一个「虚拟页」。
- **符合度**:方案完全符合;当前基本符合,方案更干净。
### 4.2 单一事实来源 (SSOT)
- **方案**`allCourseNodes` 仅课程章节;完成页由导航栈 + 手势驱动,不进入数据模型。
- **当前**`allCourseNodes` 已是纯净的 `flatMap` 章节节点,未追加 placeholder node。
- **符合度**:方案与当前都符合;方案在视图层也不再依赖「多一页」的 tagSSOT 更纯粹。
### 4.3 响应式导航
- **方案**`CompletionView` 通过 `Binding<NavigationPath>` 执行 `removeLast(2)`,栈由 path 唯一决定,无 `switchToGrowthTab()` + `dismiss()` 的二次操作。
- **当前**:依赖 `dismiss()` 回播放器,再从播放器回地图,存在中间层与潜在白屏/闪烁。
- **符合度**:方案明显更符合;当前有改进空间。
### 4.4 原生手感
- **方案**:最后一节用 `DragGesture`(或类似)识别左滑,不依赖「滑到下一 tab 索引」才触发。
- **当前**:依赖用户滑到「占位页」才触发,仍与 TabView 索引/选页绑定。
- **符合度**:方案更贴近「手势驱动」;当前是「页面索引 + 手势」混合。
---
## 五、与既有文档的一致性
### 5.1 COMPLETION_VIEW_UI_LAYER_SPEC.md
- 说明要求:返回用 `dismiss()`**不**接收或操作 `NavigationPath`
- **方案**:改为接收 `Binding<NavigationPath>``removeLast(2)`,与当前 spec 冲突。
- **建议**:若采用方案,需**同步更新** COMPLETION_VIEW_UI_LAYER_SPEC
- 写明「穿透式返回」为可选/推荐实现;
- 接口增加 `@Binding var navigationPath: NavigationPath`
- 底部按钮行为改为「可调用 `removeLast(2)` 直接回地图」,并注明调用方需传入对应 path 的 Binding。
### 5.2 COMPLETION_PAGE_MEANING.md
- 当前描述为:最后一节后再左滑一页进入占位页,占位页触发 push。
- **方案**:最后一节左滑即触发(或先切 tag 再触发),可完全去掉「多一页」概念。
- **建议**:若采用方案,更新 COMPLETION_PAGE_MEANING
- 「最后一页」仍指课程的最后一节;
- 进入完成页的方式改为「在最后一节左滑(手势)触发」,并注明是否保留占位页。
---
## 六、风险与注意点
1. **多入口 path**
CompletionView 在 Growth / Profile / Discovery 三处被 present需分别传入 `growthPath` / `profilePath` / `homePath` 的 Binding漏传或传错会导致 `removeLast(2)` 作用在错误栈上。建议:
- 在审查/实现时逐处确认传参;
- 或为 CompletionView 封装「当前栈」来源(例如 Environment 注入当前 path避免调用方误绑。
2. **NavigationPath.count**
`removeLast(2)` 前需保证 `path.count >= 2`(完成页 + 播放器)。若从深链或异常入口进入完成页,栈深度可能不足,需保留兜底(如 `dismiss()`)。
3. **手势与 TabView 滑动冲突**
最后一节左滑既会触发自定义手势,也是 TabView 的翻页手势。需通过 `minimumDistance`、`coordinateSpace` 或「边缘优先」等策略区分,避免误触或重复触发。建议在真机多测:最后一节左滑、快速连续左滑、斜滑。
4. **从完成页返回后的 Tab 状态**
当前实现:从完成页 dismiss 回播放器后,`onAppear` 里若 `currentNodeId == "wg://completion"` 会切回 `allCourseNodes.last?.id`。采用方案后,若使用 `removeLast(2)`,不会回到播放器,故无需再依赖该逻辑;若仍保留「先 dismiss 再回地图」的入口,需保留或等价处理该状态恢复。
---
## 七、审查结论与建议
| 项目 | 结论 |
|------|------|
| **架构与标准** | 方案满足「零副作用、SSOT、响应式导航、原生手感」四项标准且比当前实现更彻底。 |
| **与现有 spec** | 与 COMPLETION_VIEW_UI_LAYER_SPEC 的「不操作 NavigationPath」冲突需更新 spec。 |
| **实现成本** | 中等CompletionView 接口与三处调用方改动VerticalScreenPlayerView 增加手势与可选移除占位页;需回归测试返回与多入口。 |
| **建议** | 若采纳方案:
1. 先更新 COMPLETION_VIEW_UI_LAYER_SPEC 与 COMPLETION_PAGE_MEANING再改代码
2. CompletionView 保留 `path.count >= 2` 判断与 `dismiss()` 兜底;
3. 手势与 TabView 的冲突在真机验证,必要时加防抖或边缘区域限制;
4. 三处 navigationDestination 的 `Binding` 传参做清单检查,避免漏传/错传。 |
---
**报告日期**:基于当前代码与所提供方案整理,未对仓库做任何代码修改。

View File

@ -1,77 +0,0 @@
# CompletionView Skeleton & Reveal 版 — 审查报告(不应用)
**审查日期**2025-01-29
**范围**Skeleton & Reveal Edition纯白卡片 + 呼吸骨架屏 + 蓝色弥散光 + 粒子庆祝)
**结论**仅审查、不修改仓库需求落实正确ParticleModifier 已按前次审查完成修复,**有一处可选视觉层级调整**。
---
## 1. 需求落实情况
| 项目 | 要求 | 定稿实现 | 结论 |
|------|------|----------|------|
| **纯白卡片** | 彻底去「脏」,回归纯白 | `background(Color.white)` | ✅ |
| **拒绝空状态** | 未激活时用骨架屏替代留白 | IdleSkeletonViewRoundedRectangle 骨架 + 呼吸透明度 | ✅ |
| **呼吸骨架** | 暗示「数据等你揭开」 | `isBreathing ? 0.3 : 0.6` 配合 `repeatForever(autoreverses: true)` | ✅ |
| **蓝色弥散光** | 激活后四周泛蓝光 | `shadow(color: isSystemOn ? brandBlue.opacity(0.25) : ..., radius: 40)` | ✅ |
| **彩带爆炸** | 激活瞬间粒子庆祝 | ConfettiExplosionCircle + Capsule | ✅ |
| **布局平衡** | 激活后数字填补按钮消失空间 | ActiveDashboard 使用 Spacer 居中,无按钮占位 | ✅ |
---
## 2. ParticleModifier 修复确认
Magic Card 版审查中的两处问题均已修正:
| 项目 | Magic Card 版问题 | Skeleton & Reveal 实现 | 结论 |
|------|--------------------|------------------------|------|
| **弧度转换** | `cos(angle)` 使用角度制 | `angleRad = angleDegrees * .pi / 180`,使用 `cos(angleRad)` | ✅ |
| **随机值稳定** | `angle`/`distance` 为计算属性,每次重算 | `init` 中生成并存入 `let angleRad`、`let distance` | ✅ |
| **粒子颜色** | `randomElement()!` 每次重绘变化 | `confettiColors[i % confettiColors.count]` 按 index 固定 | ✅ |
---
## 3. 逻辑继承
| 项目 | 结论 |
|------|------|
| **持久化 Key** | ✅ `has_revealed_course_\(courseId)` |
| **游客短路** | ✅ 0.5s 延迟,`performActivation(0, 0)`,不调网络 |
| **登录用户** | ✅ 0.8s 延迟 → fetchUserProfile → performActivation |
| **checkSystemStatus** | ✅ 已激活时恢复最终值 |
| **数字滚动** | ✅ `withAnimation(.linear(1.5))` + RollingNumberText |
---
## 4. 可选:粒子与文字层级
**当前实现**`ConfettiExplosion` 使用 `.zIndex(2)`,卡片内容 VStack 使用默认 zIndex因此粒子叠在文字之上。
**注释意图**:「粒子炸在文字后面,但在卡片背景前面」——若希望粒子在文字后面,需要降低粒子的 zIndex或提高卡片内容的 zIndex。
**建议**:若需粒子在文字后面,可为卡片 VStack 添加 `.zIndex(1)`,并为 `ConfettiExplosion` 使用 `.zIndex(0)` 或不设置。若希望粒子在前面以增强庆祝感,可保持现状。属视觉偏好,非必须修改。
---
## 5. 其他实现细节
| 项目 | 说明 |
|------|------|
| **骨架 blur(radius: 3)** | 增加模糊感,在多数设备上可接受;低端机若有卡顿再考虑去掉 |
| **呼吸动画** | `onAppear``withAnimation(.easeInOut(duration: 1.0).repeatForever(autoreverses: true))` 驱动 `isBreathing`,逻辑正确 |
| **ScaleButtonStyle** | 未使用,无重复定义风险 |
| **接口** | 三参数不变,可直接替换 |
---
## 6. 总结
| 维度 | 结论 |
|------|------|
| **需求落实** | ✅ 纯白、骨架屏、弥散光、粒子、布局均符合 |
| **ParticleModifier** | ✅ 弧度、随机稳定、颜色已修正 |
| **逻辑继承** | ✅ Key、游客、登录用户、数字滚动正确 |
| **粒子层级** | ⚪ 可选:按需求调整 zIndex 以实现「粒子在文字后面」 |
| **接口兼容** | ✅ 可直接替换 |
**审查结论**Skeleton & Reveal 版满足设计要求ParticleModifier 已正确修复,可直接使用;如需粒子在文字后方,可按上节建议调整 zIndex。**本次未对仓库做任何修改。**

View File

@ -1,64 +0,0 @@
# CompletionView Y2K 版 — 最终候选版 (FRC) 审查报告(不应用)
**审查日期**2025-01-29
**范围**Y2K Final Release Candidate含持久化 Key 回滚 + 游客短路逻辑补全)
**结论**:仅审查、不修改仓库;两项修复已正确落实,可视为可发布候选。
---
## 1. 持久化 Key 回滚 ✅
| 项目 | 要求 | FRC 实现 | 结论 |
|------|------|----------|------|
| **storageKey** | 与现版一致,沿用 `has_revealed_course_\(courseId)` | `private var storageKey: String { "has_revealed_course_\(courseId)" }` | ✅ 正确 |
从拍立得版本升级到 Y2K 后,已显影过的课程会直接显示结果,无需再次点击 SYNC。
---
## 2. 游客短路逻辑 ✅
| 项目 | 要求 | FRC 实现 | 结论 |
|------|------|----------|------|
| **是否调网络** | 游客不调 `fetchUserProfile` | `if userManager.isGuest { ... return }` 先判断,仅主线程延迟后 `finalizeSync()` | ✅ 不调接口 |
| **视觉延迟** | 极短“假连接”(你要求 0.5s | `DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { finalizeSync() }`0.6s | ✅ 实现合理;若需严格 0.5s 可将 `0.6` 改为 `0.5` |
| **结果与持久化** | 直接成功并写 Key | `finalizeSync()``isSystemOn = true`、`UserDefaults.set(true, forKey: storageKey)`、触觉反馈 | ✅ 一致 |
游客路径:点击 → `isBooting = true` → 0.6s 后主线程执行 `finalizeSync()` → 显影 + 写 Key + 成功反馈,无任何网络请求。
---
## 3. 登录用户路径 ✅
- `Task.sleep(1.2s)``fetchUserProfile()`catch 忽略)→ `MainActor.run { finalizeSync() }`
- 与现版“拉取后显影”一致,无变更。
---
## 4. 结构与线程安全 ✅
| 项目 | 说明 |
|------|------|
| **finalizeSync()** | 集中处理显影 + 写 Key + `isBooting = false` + 触觉,避免重复;仅从主线程/主队列调用(`DispatchQueue.main.asyncAfter` 与 `MainActor.run`),对 `@State` 的更新安全。 |
| **SpeakerGrill(rotation:)** | 抽取为 `private func SpeakerGrill(rotation: Double) -> some View`,在 body 中调用合法,无问题。 |
---
## 5. 可选小修正(非必须)
- **延迟时长**需求写“0.5s 假连接”,代码为 0.6s;若需严格一致,可将 `deadline: .now() + 0.6` 改为 `0.5`
- **ForEach**:若当前 Swift/SwiftUI 版本对 `ForEach(0..<n)` 报错或告警,可为 `ForEach(0..<3, id: \.self)`、`ForEach(0..<5, id: \.self)`、`ForEach(0..<80, id: \.self)` 补上 `id: \.self`
---
## 6. 总结
| 维度 | 结论 |
|------|------|
| **持久化 Key** | ✅ 已回滚为 `has_revealed_course_\(courseId)`,与现版兼容 |
| **游客短路** | ✅ 已补全:不调网络、短延迟后直接 `finalizeSync()` |
| **登录用户** | ✅ 行为与现版一致 |
| **接口与调用方** | ✅ 三参数不变,仅替换 `CompletionView.swift` 即可 |
| **其他页面** | ✅ 无影响 |
**审查结论**FRC 已正确落实“Key 回滚”与“游客短路”两项建议,逻辑与现版对齐,可作为最终发布候选。**本次未对仓库做任何修改。**

View File

@ -1,72 +0,0 @@
# CompletionView Y2K 版 — 审查报告(不应用)
**审查日期**2025-01-29
**范围**Y2K 风格完结页设计稿与附带的 Swift 代码
**结论**:仅审查、不修改仓库;以下为兼容性与逻辑核对结果。
---
## 1. 接口与调用方兼容性
| 项目 | 当前仓库 | Y2K 提案 | 结论 |
|------|----------|----------|------|
| **CompletionView 初始化参数** | `courseId: String`, `courseTitle: String?`, `navigationPath: Binding<NavigationPath>?` | 与提案一致3 个参数,同名同类型) | ✅ 兼容 |
| **VerticalScreenPlayerView 调用** | `CompletionView(courseId:, courseTitle:, navigationPath:)` | 无需修改 | ✅ 可直接编译 |
| **GrowthView / 其他 navigationDestination** | 若存在 `.completion` 且传上述 3 参数 | 无需修改 | ✅ 无影响 |
替换为 Y2K 版后,仅需整文件替换 `CompletionView.swift`**无需改任何调用处**。
---
## 2. 数据与依赖一致性
| 项目 | 当前实现 | Y2K 提案 | 结论 |
|------|----------|----------|------|
| **UserManager** | `UserManager.shared``studyStats.(time, lessons)` | 相同 | ✅ |
| **导航** | `navStore.switchToGrowthTab()` + `navigationPath?.wrappedValue = NavigationPath()``dismiss()` | 与提案一致 | ✅ |
| **持久化 key** | `has_revealed_course_\(courseId)` | `has_y2k_booted_\(courseId)` | ⚠️ 见下 |
- **持久化 key 不同**:若从当前「赛博拍立得」版直接切到 Y2K 版用户在本课程下会视为「未启动过」Y2K需再按一次 SYNC 才会显示结果。若希望换肤后「已显影过的课程仍为已显影」,可把 Y2K 的 `storageKey` 改为与现版一致(例如继续用 `has_revealed_course_\(courseId)`),否则保留 `has_y2k_booted_\(courseId)` 亦可,属产品选择。
---
## 3. 逻辑差异(游客与加载)
| 项目 | 当前实现 | Y2K 提案 | 建议 |
|------|----------|----------|------|
| **游客 (isGuest)** | 点击后不调 API直接显影并写持久化 | 仍执行 `Task.sleep` + `fetchUserProfile()`catch 后置 `isSystemOn = true` | 建议在 Y2K 的 `startUpload()` 内保留与现版一致的游客分支:若 `userManager.isGuest` 则直接设 `isSystemOn = true` 并写持久化,不调 `fetchUserProfile`,避免无谓请求与约 1.2s 延迟。 |
| **登录用户** | 拉取 `fetchUserProfile` 后显影 | 同1.2s 后拉取 + 显影) | ✅ 行为一致 |
若严格遵循「只换皮肤、逻辑不变」,建议在 Y2K 版中补上与现版相同的 `isGuest` 短路逻辑。
---
## 4. UI / 实现细节核对
- **Y2K 外壳与屏幕**:半透明渐变 + `strokeBorder` 高光 + `.ultraThinMaterial`、LCD 开/关色、果冻按钮与 SYNC DATA/进度态,均为纯视觉,无逻辑影响。
- **Y2KIdleView / Y2KResultView**:使用 `userManager.studyStats.lessons` / `time` 与当前数据源一致;字体与 XP 风格进度条仅为样式。
- **ForEach(0..<n)**:提案中 `ForEach(0..<3)`、`ForEach(0..<5)`、`ForEach(0..<80)` 等以 `Range<Int>` 配合 `id: \.self` SwiftUI 中合法无需改
- **屏幕内布局**:内屏 `overlay` 中再 `.padding(12)` 会形成双层内边距;若希望与现版内边距一致,可后续微调数值,非阻塞。
---
## 5. 影响范围(仅换 CompletionView 时)
- **仅替换** `Views/CompletionView.swift` 为 Y2K 版时:
- **VerticalScreenPlayerView**:无需改动。
- **MapView / MainTabView / GrowthView / ProfileView / DiscoveryView**:无改动、无影响。
- 若采纳「持久化 key 统一」或「游客短路」建议,仅需在 Y2K 版单文件内修改,不涉及其他页面。
---
## 6. 总结
| 维度 | 结论 |
|------|------|
| **接口兼容** | ✅ 三参数一致,调用方无需修改 |
| **数据与导航** | ✅ 与现版一致 |
| **持久化 key** | ⚠️ 与现版不同,换肤后需再按一次 SYNC可按需统一 key |
| **游客逻辑** | ⚠️ 建议在 Y2K 版中保留与现版相同的 isGuest 短路,实现「只换皮肤」 |
| **其他页面** | ✅ 无影响 |
**审查结论**Y2K 版可作为「仅换皮肤」的替换方案;建议在应用前(若采纳)补上游客短路逻辑,并按产品需求决定是否统一持久化 key。**本次未对仓库做任何修改。**

View File

@ -1,136 +0,0 @@
# 1.30dazhi合并前 · 完结页相关变更清单(禁止应用)
**说明**:根据 git 拉取的 **1.30dazhi合并前** 分支与 commit **f725f31** 核对:除完结页外,**其他页面是否有被影响**。
**结论**:仅做审查与清单整理,**绝对禁止应用任何代码**。
---
## 一、当前分支与提交
- **当前分支**`1.30dazhi合并前`
- **完结页相关提交**`f725f31` — `feat: 完成页逻辑与占位页进入、无完成按钮;课程完成导航与文档`
- **工作区**:有 **1 个未提交修改**`ios/WildGrowth/WildGrowth/VerticalScreenPlayerView.swift`(约 41 行变更)
---
## 二、f725f31 中「除完结页外」被改动的文件(其他页面)
**f725f31** 里,**除了** 新增的 `CompletionView.swift` 和文档、以及 `VerticalScreenPlayerView.swift` 的完成页逻辑外,**以下页面/文件也被一起改动了**
### 1. CourseNavigation.swift
| 变更 | 说明 |
|------|------|
| `.player` | 由 `(courseId, nodeId)` 改为 `(courseId, nodeId, isLastNode, courseTitle)` |
| 新增 `.completion` | `(courseId, courseTitle, completedLessonCount)` |
| 注释 | 「从 HomeView 提取」改为「供 DiscoveryView / GrowthView / MapView / ProfileView 共用」 |
**影响**:所有使用 `CourseNavigation.player` / `.completion` 的调用方必须传新参数或处理新 case。
---
### 2. MainTabView.swift
| 变更 | 说明 |
|------|------|
| Tab 选中状态 | 由 `@State private var selection` 改为使用 `navStore.selectedTab` |
| 新增 `NavigationStore` | `@Published var selectedTab`、`func switchToGrowthTab()` |
| TabView 绑定 | `TabView(selection: $selection)``TabView(selection: $navStore.selectedTab)` |
| 登录成功跳转 | `selection = 2``navStore.selectedTab = 2` |
| 多处 print | `selection``navStore.selectedTab` |
**影响**Tab 切换与完成页「回到技能页」依赖 `navStore.selectedTab``switchToGrowthTab()`
---
### 3. MapView.swift
| 变更 | 说明 |
|------|------|
| 点击小节进入播放器 | `CourseNavigation.player(courseId, nodeId)``CourseNavigation.player(courseId, nodeId, isLastNode, courseTitle)` |
| 新增 `isLastNode` | `(chapter.id == data.chapters.last?.id) && (index == chapter.nodes.count - 1)` |
| 传入 `courseTitle` | `data.courseTitle` |
**影响**:从地图进播放器时,会多传 `isLastNode`、`courseTitle`,与 `CourseNavigation` 新签名一致。
---
### 4. ProfileView.swift
| 变更 | 说明 |
|------|------|
| `.map` | `MapView(courseId:)``MapView(courseId:, navigationPath: $navStore.profilePath)` |
| `.player` | `VerticalScreenPlayerView` 增加 `navigationPath: $navStore.profilePath`、`isLastNode`、`courseTitle` |
| 新增 `.completion` | `CompletionView(courseId, courseTitle, completedLessonCount)` |
**影响**:我的 Tab 下地图、播放器、完成页的导航与传参全部按新枚举和完成页流程改过。
---
### 5. DiscoveryView.swift
| 变更 | 说明 |
|------|------|
| `.map` | `MapView(courseId:)``MapView(courseId:, navigationPath: $navStore.homePath)` |
| `.player` | `VerticalScreenPlayerView` 增加 `isLastNode`、`courseTitle`(原已有 navigationPath |
| 新增 `.completion` | `CompletionView(courseId, courseTitle, completedLessonCount)` |
**影响**:发现 Tab 下地图、播放器、完成页的导航与传参全部按新枚举和完成页流程改过。
---
### 6. GrowthView.swift
| 变更 | 说明 |
|------|------|
| `.player` | `VerticalScreenPlayerView` 增加 `isLastNode`、`courseTitle`(原已有 MapView navigationPath |
| 新增 `.completion` | `CompletionView(courseId, courseTitle, completedLessonCount)` |
**影响**:技能 Tab 下播放器、完成页的导航与传参全部按新枚举和完成页流程改过。
---
### 7. VerticalScreenPlayerView.swiftf725f31 内)
| 变更 | 说明 |
|------|------|
| 占位页 | `CompletionPlaceholderPage` 由「带 courseId/courseTitle/navigationPath + onAppear 里 push」改为「纯视觉占位」 |
| 新增 | TabView 上 `.onChange(of: currentNodeId)`,当 `newId == "wg://completion"` 时 0.1s 后 append `.completion` |
**影响**:完成页进入方式从「占位页 onAppear」改为「父视图 onChange」避免预加载误触。
---
## 三、当前工作区未提交改动(仅 1 个文件)
| 文件 | 变更概要 |
|------|----------|
| **VerticalScreenPlayerView.swift** | 在 f725f31 基础上CompletionPlaceholderPage 去掉参数、改为纯 ZStackTabView 增加 `.onChange(of: currentNodeId)` 的完成页 push 逻辑与上面「f725f31 内」描述一致,可能是同一逻辑的又一次提交前微调或重复修改) |
**其他页面**:当前 **无** 未提交修改。GrowthView、ProfileView、DiscoveryView、MapView、MainTabView、CourseNavigation 等在工作区均为已提交状态f725f31
---
## 四、直接回答「除了完结页,其他页面有没有被影响」
**有。**
在引入完结页的 **f725f31** 中,除了:
- 新增:`CompletionView.swift`、`COMPLETION_PAGE_MEANING.md`、`COMPLETION_VIEW_*` 等文档
- 修改:`VerticalScreenPlayerView.swift`(占位页 + 完成页进入逻辑)
还**一并修改了**以下「其他页面」以支持完成页导航与播放器参数:
1. **CourseNavigation.swift** — 枚举增加 `.completion`、`.player` 增加 `isLastNode`、`courseTitle`
2. **MainTabView.swift** — Tab 状态迁到 `navStore.selectedTab`,新增 `switchToGrowthTab()`
3. **MapView.swift** — 进播放器时传 `isLastNode`、`courseTitle`
4. **ProfileView.swift** — MapView 传 navigationPath播放器传 isLastNode、courseTitle增加 `.completion` destination
5. **DiscoveryView.swift** — MapView 传 navigationPath播放器传 isLastNode、courseTitle增加 `.completion` destination
6. **GrowthView.swift** — 播放器传 isLastNode、courseTitle增加 `.completion` destination
因此:**1.30dazhi合并前** 分支上,完结页不是「只动完结页」,而是**和上述 6 个文件一起**在 f725f31 里改的;当前工作区里,只有 **VerticalScreenPlayerView.swift** 还有未提交修改,其他页面没有新的未提交变更。
---
**未对仓库进行任何应用或修改操作。**

View File

@ -1,107 +0,0 @@
# CompletionView iOS 原生风格版 — 定稿审查报告(不应用)
**审查日期**2025-01-29
**范围**Final iOS Native StylesystemGroupedBackground + 白卡片 + System Blue + Widget 布局)
**结论**:仅审查、不修改仓库;需求落实正确,**有一处必须修复**ScaleButtonStyle 重复定义)。
---
## 1. 需求落实情况
### 视觉风格
| 项目 | 要求 | 定稿实现 | 结论 |
|------|------|----------|------|
| **背景** | systemGroupedBackground 浅灰 | `Color(UIColor.systemGroupedBackground)` | ✅ |
| **卡片** | 纯白圆角 + 柔和投影 | `cardBg = .white``.cornerRadius(16)``shadow(radius:15, y:6)` | ✅ |
| **配色** | System Blue品牌蓝 | `brandBlue = Color.blue` | ✅ |
| **布局** | Widget 小组件式 | 卡片头(学习统计 + chart 图标)+ 主视觉(小节数)+ 次视觉(专注时长) | ✅ |
| **主/次视觉** | 超大小节数 + 底部专注时长 | 80pt 数字 + 底部一行「专注时长 X min」 | ✅ |
### 交互与动效
| 项目 | 要求 | 定稿实现 | 结论 |
|------|------|----------|------|
| **按钮文案** | 上传数据 → Loading → 已同步 | 三态:`Text("上传数据")` / `ProgressView` / `HStack(checkmark + "已同步")` | ✅ |
| **按钮颜色** | 严禁变绿,始终蓝底 | `background(brandBlue)` | ✅ |
| **屏幕动效** | 数据加载完成,内容淡入 | `withAnimation(.easeOut(0.3)) { isSystemOn = true }` | ✅ |
| **数字动效** | 从 0 匀速滚动到目标1.5s | `withAnimation(.linear(1.5)) { displayLessons/Minutes }` + `RollingNumberText` | ✅ |
### 业务逻辑
| 项目 | 要求 | 定稿实现 | 结论 |
|------|------|----------|------|
| **数据源** | UserManager.shared | `@ObservedObject userManager = UserManager.shared` | ✅ |
| **持久化** | UserDefaults 防重复动效 | `storageKey: "has_revealed_course_\(courseId)"` | ✅ |
| **导航** | `navStore.switchToGrowthTab()` | 一致 | ✅ |
---
## 2. 逻辑继承FRC 要求)
| 项目 | 结论 |
|------|------|
| **持久化 Key** | ✅ `has_revealed_course_\(courseId)` |
| **游客短路** | ✅ 0.5s 延迟,`performActivation(0, 0)`,不调网络 |
| **登录用户** | ✅ 0.8s 延迟 → fetchUserProfile → performActivation |
| **checkSystemStatus** | ✅ 已激活时恢复最终值,无动画 |
---
## 3. ⚠️ 必须修复ScaleButtonStyle 重复定义
**问题**:定稿代码在文件末尾定义了 `struct ScaleButtonStyle: ButtonStyle`,而项目中已在 `NotebookListView.swift`(约第 287 行)定义同名 struct。同一 target 内存在两个 `ScaleButtonStyle` 会导致 **invalid redeclaration** 编译错误。
**修复**:删除 CompletionView.swift 中 `ScaleButtonStyle` 的完整定义(`struct ScaleButtonStyle: ButtonStyle { ... }`),直接使用项目已有的实现。`ScaleButtonStyle` 在同一模块内可访问,无需额外导入。
---
## 4. RollingNumberText
- `RollingNumberText` 仅在 CompletionView 相关代码中使用,当前实现 `Animatable` 正确,与定稿逻辑一致。
- 保留在 CompletionView.swift 内不会产生符号冲突。
---
## 5. 模块化与 HIG 符合度
- 将 `SummaryCardView`、`ActiveDashboardView`、`IdlePlaceholderView`、`UploadButton` 拆分为 computed property结构清晰。
- 使用 `Color(UIColor.systemGroupedBackground)`、`Color(UIColor.secondarySystemGroupedBackground)` 等系统颜色,符合 iOS HIG。
- `ScaleButtonStyle` 的按压缩放效果与 NotebookListView 中的实现一致,视觉体验统一。
---
## 6. 接口与影响范围
| 项目 | 结论 |
|------|------|
| **初始化参数** | 三参数不变 |
| **替换方式** | 整文件替换 `CompletionView.swift` |
| **其他页面** | 无影响 |
---
## 7. 总结
| 维度 | 结论 |
|------|------|
| **需求落实** | ✅ 视觉、交互、动效、业务逻辑均符合要求 |
| **逻辑继承** | ✅ Key、游客、登录用户逻辑正确 |
| **ScaleButtonStyle** | ❌ **必须删除定稿中的定义**,改用项目已有实现 |
| **接口兼容** | ✅ 可直接替换 |
**审查结论**:定稿在逻辑和需求上正确,应用前需**删除 CompletionView.swift 中的 `struct ScaleButtonStyle` 定义**,否则无法通过编译。**本次未对仓库做任何修改。**
---
## 8. 修正版审查确认2025-01-29 续)
**修正内容**:已移除 `ScaleButtonStyle` 结构体定义,仅保留 `.buttonStyle(ScaleButtonStyle())` 调用,复用 `NotebookListView.swift` 中已有实现。
| 项目 | 修正前 | 修正后 | 结论 |
|------|--------|--------|------|
| **ScaleButtonStyle** | 文件末尾重复定义 | 已删除,使用项目已有 | ✅ 修复完成 |
| **调用处** | `.buttonStyle(ScaleButtonStyle())` | 不变,解析为 NotebookListView 中定义 | ✅ 正确 |
| **注释** | — | 添加「使用项目已有」「此处不重复声明」 | ✅ 便于后续维护 |
**结论**:修正版可**直接替换** `CompletionView.swift`,无编译冲突。**本次未对仓库做任何修改。**

View File

@ -1,129 +0,0 @@
# 完成页导航方案 — 实现后行为与影响说明
说明:实现方案后**会变成什么样**、**会不会影响原有逻辑和展示**、**会不会导致其他地方出问题**。不修改代码,仅作说明。
---
## 一、实现后会变成什么样
### 1. 进入完成页(两种方式,二选一或并存)
| 方式 | 当前 | 实现后(若保留占位页 + 加手势) |
|------|------|----------------------------------|
| **左滑到「下一页」** | 最后一节再左滑 → 进入占位页(空白)→ 0.1s 后 push 完成页 | 不变:仍可滑到占位页,再由 `onChange` push |
| **在最后一节左滑** | 无 | 新增:在最后一节直接左滑(手势识别)→ 可设为「切到占位页再 push」或「直接 push 完成页」 |
若方案采用「删除占位页、仅手势」:
- 最后一节左滑 → 直接 push 完成页,**不再出现空白占位页那一屏**。
- TabView 只有真实课程页,不再有「多一页」的占位。
**展示上**:完成页本身 UI 不变(仍是当前 CompletionView 的 3D 卡片、打字机等);变的只是「怎么进」和「怎么回」。
---
### 2. 从完成页返回(核心变化)
| 操作 | 当前 | 实现后 |
|------|------|--------|
| **顶部返回 (chevron)** | `dismiss()` → 回到播放器(最后一节或占位页) | 可保持不变:仍 `dismiss()`,回到播放器 |
| **底部「继续学习」** | `navStore.switchToGrowthTab()` + `dismiss()` | `handleReturnToMap()``navigationPath.removeLast(2)` |
**「继续学习」行为对比**
- **当前**
1. `dismiss()` → 栈 pop 一层,回到 **VerticalScreenPlayerView**(当前 Tab 可能是占位页,`onAppear` 里会切回最后一节)。
2. `switchToGrowthTab()` → 切到「技能」Tab**清空 `growthPath`**`growthPath = NavigationPath()`)。
3. 用户最终看到的是 **技能 Tab 的根界面(课程列表)**,不是地图。
- **实现后**
1. `removeLast(2)` → 一次 pop 掉「完成页」和「播放器」。
2. 栈变成:**[MapView]**(或更短,视当前 path 而定)。
3. **不**调用 `switchToGrowthTab()`,所以**不会清空 path**,也**不一定会切 Tab**。
4. 用户最终看到的是 **当前 Tab 下的地图页**(从哪个 Tab 进的,就还在哪个 Tab
**总结**
- 实现后,点「继续学习」会**直接回到地图**,且**保留在当前 Tab**(发现 / 技能 / 我的)。
- 当前是**回到课程列表**(且强制在技能 Tab。这是**产品行为上的明确变化**。
---
### 3. 三条入口分别会怎样
| 入口 | 当前栈(到完成页时) | 当前「继续学习」后 | 实现后「继续学习」后 |
|------|----------------------|--------------------|------------------------|
| **技能 Tab** | growthPath: [Map, Player, Completion] | 切到技能 Tab + 清空栈 → **课程列表** | 仍技能 Tab栈 [Map] → **地图** |
| **发现 Tab** | homePath: [Map, Player, Completion] | dismiss 回播放器switchToGrowthTab 切到技能并清空 → **课程列表** | 仍发现 Tab栈 [Map] → **地图** |
| **我的 Tab** | profilePath: [Map, Player, Completion] | 同上 → **课程列表** | 仍我的 Tab栈 [Map] → **地图** |
---
## 二、会不会影响原有逻辑和展示
### 1. 会改变的部分(有意为之)
- **底部按钮语义**:「继续学习」从「回课程列表(并切到技能)」变为「回地图(且留在当前 Tab」。
- **是否清空栈**:不再在完成页里清空 `growthPath`,所以不会出现「从完成页一点就回到空白课程列表」。
- **是否切 Tab**:不再强制切到技能 Tab用户会留在发现 / 技能 / 我的中的当前 Tab。
若产品期望就是「完成课后回到地图、留在当前 Tab」则与方案一致若期望是「回到课程列表、且一定在技能 Tab」则与**当前**一致,与**实现后**不一致,需要产品确认。
### 2. 可保持不变的部分
- **完成页 UI**3D 翻转卡片、打字机、顶部返回、按钮样式等都可以不改。
- **顶部返回**:继续用 `dismiss()`,仍回到播放器,逻辑和展示都不变。
- **进入完成页的方式**:若保留占位页,现有「滑到占位页再 push」仍可用只是多了一种「最后一节左滑」的触发方式若加手势
### 3. 依赖「当前行为」的地方
- **NavigationStore.switchToGrowthTab()**:当前只在 CompletionView 的「继续学习」里被调用。实现后 CompletionView 不再调用它,**不会影响其他使用处**(因为别处没有用这个方法)。
- **growthPath 被清空**:当前只有「完成页点继续学习」会清空 growthPath。若没有「从完成页返回后依赖 growthPath 为空」的逻辑,则实现后**不会破坏其它功能**。
---
## 三、会不会导致其他地方出问题
### 1. 三个 Tab 的 navigationDestination必须改
- **GrowthView / ProfileView / DiscoveryView**`.completion` 分支都要给 CompletionView 传 **Binding**
`navigationPath: $navStore.growthPath` / `$navStore.profilePath` / `$navStore.homePath`
- **漏传或错传**
- 漏传 → 编译不通过CompletionView 多了必选参数)。
- 错传(例如发现进的完成页却传了 `growthPath`)→ 点「继续学习」会 pop 错栈,可能白屏或回到错误 Tab。
- **结论**:三处都必须改,且必须传**当前栈对应的 path**,否则会出问题。
### 2. 笔记流NoteTreeView / NoteListView— 不受影响
- 笔记流用的是 **NoteNavigationDestination.player**,打开的是 **VerticalScreenPlayerView**,且**没有传 navigationPath**(或传的是笔记自己的 path
- 在这些地方打开的播放器里,`navigationPath` 为 nil现有逻辑里 `onChange` 里会 `guard let path = navigationPath else { return }`**不会 push CourseNavigation.completion**。
- 因此:从笔记进的播放器,**不会**出现「滑到完成页」的 push除非你后续在笔记流里也接 Course 的 completion
- **结论**:实现完成页方案**不会影响笔记流**,也不会从笔记流误进完成页。
### 3. 从完成页返回时栈深度不足
- 正常流程栈至少为 [Map, Player, Completion]`removeLast(2)` 安全。
- 若将来有「深链、通知、分享」等直接打开 CompletionView栈可能只有 [Completion],此时 `path.count >= 2` 为假,应走兜底 `dismiss()`
- 方案里已建议保留 `if navigationPath.count >= 2 { removeLast(2) } else { dismiss() }`**不会因为栈浅而崩溃**,最多退回单层 pop。
### 4. 手势与 TabView 滑动
- 在最后一节加「左滑进完成页」时,与 TabView 自带的左滑翻页**共用同一方向**,可能冲突(例如:想翻页却触发了完成页,或想进完成页却只翻到占位页)。
- 若用「边缘左滑」或阈值(如 -60pt、防抖可减轻误触需在真机多测。
- **结论**:可能带来**体验上的小问题**(误触/难触),不是逻辑错误,可通过手势参数和测试收敛。
### 5. 其他使用 VerticalScreenPlayerView / CompletionView 的地方
- **VerticalScreenPlayerView**:除 GrowthView / ProfileView / DiscoveryView 外,仅在 **NoteTreeView / NoteListView** 出现,且走的是 NoteNavigationDestination不涉及 CourseNavigation.completion**不受影响**。
- **CompletionView**:只在这三个 Tab 的 `navigationDestination(for: CourseNavigation.self)``.completion` 分支出现,**没有其它调用点**。
- **结论**:只要三处传参正确,**不会导致其它页面逻辑错误**。
---
## 四、简要结论表
| 问题 | 结论 |
|------|------|
| 实现后会变成什么样? | 进入完成页可增加「最后一节左滑」触发;「继续学习」从「回课程列表 + 切技能 Tab」变为「直接回地图 + 保留当前 Tab」可选去掉占位页。 |
| 会不会影响原有逻辑和展示? | 会:底部按钮语义和最终停留页(地图 vs 课程列表)、是否清栈/切 Tab 会变;顶部返回和完成页 UI 可保持不变。 |
| 会不会导致其他地方出问题? | 三处传 Binding 必须正确,否则会 pop 错栈;笔记流、其它使用点不受影响;栈浅时用 dismiss 兜底;手势可能与 TabView 滑动冲突,需真机调参。 |
**建议**:实现前与产品确认「继续学习」的预期是「回地图」还是「回课程列表」;若确认为回地图且保留当前 Tab再按方案改并保证三处 path 传参一致。

View File

@ -1,82 +0,0 @@
# 完成页「强力侧滑版」— 审查报告(禁止应用)
**审查对象**:粉紫勋章视觉 + RobustSwipeBackEnablerdidMove + viewDidAppear 双节点强制开启侧滑)
**结论**:仅审查,不修改仓库内任何文件。
---
## 一、问题与方案对应
| 问题 | 方案 | 审查结论 |
|------|------|----------|
| 隐藏导航栏后侧滑返回失效 | 用 `UIViewController` 子类在 `didMove(toParent:)``viewDidAppear` 两处调用 `enableGesture()` | ✅ 思路正确,双节点可提高找回手势的概率 |
| 之前 SwipeBackEnabler 时机不对或被覆盖 | 使用自定义 `SwipeBackController` 继承 `UIViewController`,在生命周期中多次执行 | ✅ 比仅用 `updateUIViewController` 的 Representable 更稳 |
---
## 二、强力侧滑实现审查
### 2.1 RobustSwipeBackEnabler + SwipeBackController
- **Representable**`makeUIViewController` 返回 `SwipeBackController()``updateUIViewController` 为空,符合「只挂一个控制器做生命周期」的用法。
- **SwipeBackController**
- `didMove(toParent:)`:视图被加入/移出父控制器时调用,此时通常已进入导航栈,可拿到 `navigationController`
- `viewDidAppear`:每次该页显示时再执行一次,可应对 SwiftUI 更新或系统把手势关掉的情况。
- **enableGesture()**
`nc.interactivePopGestureRecognizer?.delegate = nil` + `isEnabled = true` 是常见做法,用于在隐藏导航栏时恢复全屏侧滑。逻辑正确。
### 2.2 潜在注意点(非否决项)
1. **视图层级**`.background(RobustSwipeBackEnabler())` 会把 `SwipeBackController` 的 view 作为 CompletionView 的底层。SwiftUI 的 NavigationStack 对应底层会有一个 `UINavigationController``self.navigationController` 应是该栈,因此一般能正确找到。若将来把 CompletionView 放在非 NavigationStack 的容器里,需再确认 `navigationController` 是否仍为预期栈。
2. **多次执行**`didMove` / `viewDidAppear` 可能被多次调用(例如 SwiftUI 重绘、present 方式变化),重复执行 `enableGesture()` 无副作用,可接受。
3. **真机验证**:建议在真机上从「发现 / 技能 / 我的」三个 Tab 分别进入完成页,各做几次右滑返回,确认无偶发失效;若仍有失效,可再在 `viewWillAppear` 或短延迟 `DispatchQueue.main.async` 中补一次 `enableGesture()`
---
## 三、视觉与交互审查
### 3.1 与需求对照
| 需求 | 实现 | 结论 |
|------|------|------|
| 粉紫勋章:点前灰色+「完成」,点后粉紫+「共完成 XX 节」 | 未激活:灰底 + 灰边 + 「完成」;激活:粉紫渐变圆 + 白字「共完成 / N 小节」+ 冲击波 | ✅ 一致 |
| 顶部无返回按钮,只有标题「已完成」 | `ZStack` 中仅 `Text("已完成")`,无 Button | ✅ 一致 |
| 底部淡蓝按钮、无箭头 | `Text("回到我的内容")``.foregroundColor(.brandVital)``Capsule().fill(Color.brandVital.opacity(0.08))`,无 SF Symbol | ✅ 一致 |
| 右滑回最后一节 | 依赖 RobustSwipeBackEnabler | ✅ 见上 |
| 底部按钮回技能页根 | `handleReturnToRoot()``navStore.switchToGrowthTab()` | ✅ 一致 |
### 3.2 配色与 DesignSystem
- 方案中本地定义:
- `cyberPink = Color(red: 1.0, green: 0.25, blue: 0.50)` → 等价 `#FF4081`
- `cyberPurple = Color(red: 0.54, green: 0.31, blue: 1.0)` → 等价 `#8A4FFF`
- **DesignSystem** 中已有:
- `Color.cyberNeon` = `#FF4081`
- `Color.cyberIris` = `#8A4FFF`
- **建议**(可选):将勋章渐变的 `cyberPink` / `cyberPurple` 改为 `Color.cyberNeon` / `Color.cyberIris`,避免重复定义并统一设计系统;若你希望完成页与 DesignSystem 解耦则可保留当前写法。
### 3.3 其他细节
- 冲击波:`rippleScale` 从 1.0 到 1.8、`rippleOpacity` 从 1.0 到 0时长 0.8s,视觉合理。
- 震动:`UIImpactFeedbackGenerator(style: .heavy)`,与「点亮」动作匹配。
- 底部按钮常驻显示(不依赖 `isActive`),与「回到我的内容」随时可点一致。
---
## 四、接口与调用方
- **CompletionView** 仍为三参:`courseId`, `courseTitle`, `completedLessonCount`,依赖 `@EnvironmentObject navStore`
- **GrowthView / ProfileView / DiscoveryView** 无需改构造或传参。
---
## 五、审查结论汇总
| 项 | 结论 |
|----|------|
| 强力侧滑方案 | 双生命周期节点didMove + viewDidAppear合理实现正确建议真机多入口验证必要时可再加 viewWillAppear 或短延迟兜底。 |
| 粉紫勋章视觉 | 与描述一致;可选改用 DesignSystem 的 cyberNeon/cyberIris 统一色值。 |
| 顶部 / 底部 / 交互 | 符合「无返回、仅标题、淡蓝按钮无箭头、底部回技能页根」的要求。 |
| 全量替换范围 | 仅替换 `CompletionView.swift` 即可VerticalScreenPlayerView 等不动。 |
**未对仓库内任何文件进行修改。**

View File

@ -1,100 +0,0 @@
# CompletionView「新中式赛博」版设计方案 — 审查报告(不应用)
## 一、结论摘要
- **接口**:与现有调用方完全兼容,无需修改 `VerticalScreenPlayerView` 等。
- **逻辑**:导航与“回到我的内容”行为与现有一致;仅交互由「点击勋章激活」改为「点击卡片 3D 翻转」。
- **建议**:设计说明中的「微小的上箭头」与代码中的 `sparkles` 不一致,若需严格按文案可改为 `arrow.up`;其余可合并。
---
## 二、接口与调用方
| 项目 | 当前 CompletionView | 提案代码 | 结论 |
|------|---------------------|----------|------|
| 入参 | `courseId`, `courseTitle?`, `completedLessonCount`, `navigationPath?` | 同左 | ✅ 一致 |
| 调用处 | `VerticalScreenPlayerView` 传入上述 4 项 | 无需改动 | ✅ 兼容 |
调用处代码保持不变即可:
```swift
CompletionView(
courseId: courseId,
courseTitle: self.courseTitle ?? mapData?.courseTitle,
completedLessonCount: UserManager.shared.studyStats.lessons,
navigationPath: navigationPath
)
```
---
## 三、行为与逻辑
| 能力 | 当前实现 | 提案实现 | 结论 |
|------|----------|----------|------|
| 清空栈 / 返回 | `handlePopToRoot()`:有 `navigationPath``path.wrappedValue = NavigationPath()`,否则 `dismiss()` | 相同 | ✅ 一致 |
| 触觉反馈 | 点击时 heavy按钮时 light | 翻转时 medium按钮时 light | ✅ 可接受 |
| 隐藏导航栏 | `.toolbar(.hidden, for: .navigationBar)` | 同左 | ✅ 一致 |
交互变化仅为表现形式:
- **当前**:点击圆形勋章 → 激活态 → 显示「共完成 X 小节」+ 底部「回到我的内容」。
- **提案**:点击卡片 → 3D 翻转 → 背面「共完成 X 小节」+ 底部「回到我的内容」(翻转后按钮由 0.6 到 1 透明度)。
语义一致,无逻辑冲突。
---
## 四、视觉与实现细节
1. **背景**
提案使用 `Color.bgPaper`,当前为 `Color.white`。与 app 内其他页面统一,更一致,✅ 合理。
2. **正面 (CardFrontView)**
- 竖排「完」「成」、180pt、serif、渐变 + 0.8 透明度、微旋转、裁切与 `drawingGroup` 均合理。
- 底部提示:「点击开启」+ 图标。设计说明为「微小的上箭头」,代码为 `Image(systemName: "sparkles")`。若需与说明严格一致,建议改为 `arrow.up` 或同时保留「点击开启」文案。
3. **背面 (CardBackView)**
- 「共完成」+ 数字 +「小节」、渐变底、装饰圆环,与当前信息一致,✅。
4. **底部按钮**
- 文案「回到我的内容」+ 箭头不变。
- 提案为白底胶囊 + 淡描边,未翻转时 `opacity(0.6)`,翻转后 1当前为渐变按钮始终可见。属设计取舍✅ 可接受。
5. **渐变色**
- 提案背面紫为 `Color(red: 0.58, green: 0.28, blue: 0.95)`,当前为 `0.62, 0.35, 1.0`,差异较小,✅ 无问题。
---
## 五、潜在风险与注意点
| 项 | 说明 | 风险 |
|----|------|------|
| `.foregroundStyle(gradient.opacity(0.8))` | `LinearGradient``.opacity(0.8)` 在 iOS 15+ 作为 `ShapeStyle` 使用无问题 | 低 |
| `rotation3DEffect` + `perspective: 0.8` | 常规用法 | 无 |
| CardFrontView 呼吸动画 | `hintOpacity` 0.3 ↔ 0.8`repeatForever(autoreverses: true)` | 无 |
| 卡片尺寸 280×400 | 与当前 200×200 圆不同,仅布局变化 | 无 |
未发现编译或运行时错误;替换为提案代码后,仅需在真机/模拟器上确认一次翻转与返回流程即可。
---
## 六、与设计说明的对照
| 设计说明 | 代码实现 | 一致性 |
|----------|----------|--------|
| 竖排超大字「完」「成」 | VStack + 180pt serif | ✅ |
| 出血/裁切感 | offset + clipShape + 圆角 | ✅ |
| 赛博粉紫渐变、降饱和度 | gradient.opacity(0.8) | ✅ |
| 底部「点击开启」 | Text("点击开启") | ✅ |
| 「微小的上箭头」 | 使用 `sparkles` 图标 | ⚠️ 不一致,可改为 `arrow.up` |
| 去掉圆圈 | 无圆圈,改为矩形卡片 | ✅ |
| 宋体 (Serif) | `.design(.serif)` | ✅ |
---
## 七、审查结论
- **可合并**:接口、导航与核心逻辑与现有一致,无破坏性变更。
- **可选修改**:将底部提示图标由 `sparkles` 改为 `arrow.up`,与「微小的上箭头」说明统一。
- **不应用**:按你要求本次仅审查,不替换 `CompletionView.swift`;若后续决定采用,可直接用提案代码替换整个文件,并视需要做上述图标小改。

View File

@ -1,76 +0,0 @@
# 完成页「最终交付 · UI 聚焦」审查报告(禁止应用)
**审查对象**:仅实现完结页 UI + 统一分页,不碰数据排序与 isFirstNodeInChapter 判定逻辑的最终交付代码。
**结论**:仅审查,不修改仓库内任何文件。核对「仅 UI、其他不变」及与当前仓库逻辑的一致性。
---
## 一、交付方承诺的「不变」项核对
| 承诺 | 本版代码 | 与当前仓库对比 | 结论 |
|------|----------|----------------|------|
| **loadMapData 不全局重排** | `realNodes = data.chapters.flatMap { $0.nodes }.filter { $0.status != .locked }`,无 `.sorted` | 当前:`allCourseNodes = data.chapters.flatMap { $0.nodes }.filter { $0.status != .locked }`,无排序 | ✅ 一致 |
| **isFirstNodeInChapter 保持原有逻辑** | `chapter.nodes.filter({ $0.status != .locked }).first`,无排序 | 当前:`validNodes = chapter.nodes.filter(...).sorted { $0.order < $1.order }`,再 `validNodes.first?.id == nodeId` | ⚠️ 见下 1.1 |
| **仅统一分页 + 完结页 UI** | 数据源改为 allItems (PlayerItem),最后一页渲染 CompletionView其余 init/错误/toast/tabBar/handleBack 等保留 | — | ✅ 符合 |
### 1.1 isFirstNodeInChapter 与「严格保持原有逻辑」的差异
- **当前仓库**:在 chapter 内先取 `validNodes = chapter.nodes.filter { $0.status != .locked }.sorted { $0.order < $1.order }`,再判断 `validNodes.first?.id == nodeId`,即「**按 order 排序后的第一章第一节**」。
- **本版交付**`chapter.nodes.filter({ $0.status != .locked }).first`,即「**数组顺序下的第一个未锁定节点**」,无 `.sorted { $0.order < $1.order }`
因此:本版 **改动了** isFirstNodeInChapter 的判定规则(从「按 order 的首节」变为「按数组顺序的首节」)。若后端或产品依赖「按 order 的首节」显示章节标题,本版可能与现有行为不一致。
**建议**:若需 **严格保持原有判定规则**,应在 isFirstNodeInChapter 内保留与当前一致的写法:
```swift
if chapter.nodes.contains(where: { $0.id == nodeId }) {
let validNodes = chapter.nodes
.filter { $0.status != .locked }
.sorted { $0.order < $1.order }
return validNodes.first?.id == nodeId
}
```
其余loadMapData 不排序、仅统一分页与完结页 UI本版已满足。
---
## 二、CompletionView 审查
- **职责**:仅展示(粉紫勋章、共完成 N 节、底部「回到我的内容」)+ `navStore.switchToGrowthTab()`,无 navigationPath、无数据处理。✅
- **接口**courseId / courseTitle / completedLessonCount 三参保留,与 CourseNavigation.completion 及三处 destination 兼容。✅
- **替换方式**:全量替换 `Views/CompletionView.swift` 即可。✅
**结论**CompletionView 符合「仅完结页 UI、其他不变」。
---
## 三、VerticalScreenPlayerView 审查
### 3.1 已符合「仅 UI + 统一分页、其他不变」
- **Init**6 参未改,外部调用零影响。✅
- **loadMapData**flatMap + filter无 sortallItems = realNodes.map(.lesson) + append(.completion)。与当前「章节顺序 + 数组顺序」一致。✅
- **错误态 / toast / hideTabBar / showTabBar / handleBack(path 优先)**:均保留。✅
- **LessonPageView**:传 `self.courseTitle ?? mapData?.courseTitle`、navigationPathheaderConfig 仍用 isFirstNodeInChapter / getChapterTitle。✅
- **CompletionView内嵌**courseId、courseTitle、completedLessonCount。✅
- **currentPositionProgress**:仅按 lesson 项计算,忽略完结页。✅
- **合并范围**:说明中已注明仅替换主视图 Struct保留 HeaderConfig、CourseProgressNavBar、LessonPageView 等。✅
### 3.2 唯一需确认处isFirstNodeInChapter
如上 1.1:当前仓库使用 **sorted { $0.order < $1.order }** 再取 first本版使用 **.filter().first**(无排序)。若要求「严格保持原有逻辑、不修改判定规则」,需在替换时保留当前 isFirstNodeInChapter 实现(含 .sorted { $0.order < $1.order })。
---
## 四、审查结论汇总
| 项目 | 结论 |
|------|------|
| **loadMapData** | 无全局排序,与当前「章节顺序 + 数组顺序」一致。✅ |
| **isFirstNodeInChapter** | 本版为「数组顺序首节点」;当前为「按 order 排序后首节点」。若需与现有行为完全一致,需保留当前的 .sorted { $0.order < $1.order } 写法。⚠️ |
| **CompletionView** | 仅 UI + 退出导航,可全量替换。✅ |
| **VerticalScreenPlayerView** | 仅替换主视图 Struct保留同文件其余类型其他逻辑错误/toast/tabBar/handleBack/传参)不变。✅ |
| **其他页面 / 功能** | 接口与调用方式未改,其他页面、其他功能不受影响。✅ |
**未对仓库内任何文件进行修改。**

View File

@ -1,103 +0,0 @@
# 最终交付代码审查反馈isFirstNodeInChapter 完全回滚版)
**审查对象**您提供的「CompletionView 全量替换 + VerticalScreenPlayerView 仅替换 Struct」的最终交付代码并已恢复 `isFirstNodeInChapter` 为含 `.sorted { $0.order < $1.order }` 的写法。
**结论**:仅反馈,**不应用、不修改**仓库内任何文件。
---
## 参数说明(白话)
| 说法 | 指什么 | 作用 |
|------|--------|------|
| **CompletionView 仍为 3 个参数** | 构造时仍是 `(courseId, courseTitle, completedLessonCount)` | GrowthView / ProfileView / DiscoveryView 里已有 `CompletionView(courseId: ..., courseTitle: ..., completedLessonCount: ...)` 的调用不用改;替换成新 UI 后接口不变,不会报错。 |
| **VerticalScreenPlayerView init 6 参** | 构造时仍是 `(courseId, nodeId, initialScrollIndex?, navigationPath?, isLastNode?, courseTitle?)` | MapView / GrowthView / DiscoveryView / ProfileView 等传入的 6 个参数(或只传前几项、后几项用默认值)都不用改;只改播放器内部实现,调用方零改动。 |
---
## 应用后仅实现以下三点、且无多余修改
若您**只**做这两步:① 全量替换 `CompletionView.swift`;② 仅替换 `VerticalScreenPlayerView.swift` 里的 **struct VerticalScreenPlayerView**(保留同文件内 HeaderConfig、LessonPageView 等其余代码),则:
| # | 需求 | 是否由本交付代码实现 | 说明 |
|---|------|----------------------|------|
| 1 | 播放器最后一个小节左滑进入完结页,从完结页右滑回到最后一个小节页 | ✅ 是 | 完结页作为 TabView 的最后一页内嵌在播放器内,左滑最后一节→完结页,右滑完结页→最后一节,无 push/pop。 |
| 2 | 完结页的 UI以及从接口/数据获取「共完成多少个小节」 | ✅ 是 | 新 UI粉紫勋章、点击点亮数量来自 `UserManager.shared.studyStats.lessons`(应用内统计,通常由学习进度接口或本地完成逻辑更新)。 |
| 3 | 底部一个按钮,点击回到技能 Tab我的课程列表 | ✅ 是 | 按钮「回到我的内容」仅调用 `navStore.switchToGrowthTab()`,切到技能 Tab。 |
**其他没有任何多余修改**
- **只动 2 个文件**`CompletionView.swift`(全量)、`VerticalScreenPlayerView.swift`(仅主 struct
- **不改动**CourseNavigation、MainTabView、MapView、ProfileView、DiscoveryView、GrowthView、NoteTreeView、NoteListView 等所有其他页面与类型;不增删导航枚举、不改 Tab 结构、不改地图/发现/个人/技能页逻辑。
- 完结页不再通过「占位页 + onChange push .completion」出现而是作为播放器 TabView 最后一页展示,因此无需也不会去改各 Tab 的 `navigationDestination(for: .completion)` 的写法(它们保留不动,只是从播放器内不再 push .completion
**结论**:应用本交付代码后,行为严格限于上述 1、2、3 三点,无其他多余修改。
---
## 一、isFirstNodeInChapter与当前仓库 100% 一致 ✅
| 对比项 | 当前仓库实现 | 您提供的交付代码 | 结论 |
|--------|--------------|------------------|------|
| 章节内节点 | 先找到包含 `nodeId` 的 chapter再在该章内取 `validNodes` | 遍历每个 chapter`validNodes` | 语义等价 |
| 排序 | `chapter.nodes.filter { $0.status != .locked }.sorted { $0.order < $1.order }` | `chapter.nodes.filter { $0.status != .locked }.sorted { $0.order < $1.order }` | ✅ 完全一致 |
| 首节判定 | `validNodes.first?.id == nodeId` | `validNodes.first?.id == nodeId`(在含该 node 的章内) | ✅ 完全一致 |
**结论**:章节判定逻辑已完全回滚到与当前仓库一致的写法(含 `.sorted { $0.order < $1.order }`),不会改变现有「按 order 的章节首节」展示行为。
---
## 二、loadMapData无全局排序与当前一致 ✅
- **当前仓库**`allCourseNodes = data.chapters.flatMap { $0.nodes }.filter { $0.status != .locked }`,无 `.sorted`
- **交付代码**`realNodes = data.chapters.flatMap { $0.nodes }.filter { $0.status != .locked }`,仅 UI 层 `items = realNodes.map { .lesson($0) }``append(.completion)`,无全局排序。
**结论**:数据顺序与当前一致,未引入按 order 的整课排序。
---
## 三、CompletionView仅 UI 更新,接口兼容 ✅
| 项目 | 说明 |
|------|------|
| 构造参数 | 仍为 `(courseId, courseTitle, completedLessonCount)`GrowthView/ProfileView/DiscoveryView 等处已有调用无需改 |
| 依赖 | 仍使用 `@EnvironmentObject private var navStore: NavigationStore` |
| 按钮 | 「回到我的内容」仅调用 `navStore.switchToGrowthTab()`,未调用 `dismiss()` |
| 说明 | 在统一分页方案下,完结页作为 TabView 最后一页内嵌展示,无 push 栈,不调用 `dismiss()` 是正确行为 |
**结论**:可全量替换 `CompletionView.swift`,仅 UI 从「翻牌 + 打字机」改为「粉紫勋章 + 点击点亮」,对外接口与行为符合预期。
---
## 四、VerticalScreenPlayerView仅替换 Struct其余保留 ✅
| 项目 | 结论 |
|------|------|
| **init** | 6 个构造参数完整保留courseId, nodeId, initialScrollIndex?, navigationPath?, isLastNode?, courseTitle?MapView/GrowthView 等调用方无需改 ✅ |
| **PlayerItem** | 枚举 `.lesson(MapNode)` / `.completion` 仅用于 UI 数据源,不参与完成数等业务逻辑 ✅ |
| **allItems** | 由 `realNodes` + 末尾 `.completion` 构成,无全局排序 ✅ |
| **currentPositionProgress** | 仅用 `lesson` 项计算,排除完结页,进度条正确 ✅ |
| **完结页展示** | 顶部进度条在 `currentNodeId == "COMPLETION_PAGE"` 时隐藏,逻辑正确 ✅ |
| **LessonPageView** | `courseTitle: self.courseTitle ?? mapData?.courseTitle` 可减少标题闪烁 ✅ |
| **错误态** | 保留加载失败 + 重试;交付代码中错误文案为 `.foregroundColor(.gray)`,当前为 `.inkSecondary`,属风格差异,可酌情统一 |
| **合并范围** | 仅替换 `struct VerticalScreenPlayerView { ... }` 及其内部的 `enum PlayerItem`**必须保留**同文件内 `HeaderConfig`、`DuolingoProgressBar`、`CourseProgressNavBar`、`LessonPageView`、`LessonSkeletonView` 等所有其他类型,不得整文件覆盖 ✅ |
---
## 五、对其他页面的影响
- **CourseNavigation**、**MainTabView**、**MapView**、**ProfileView**、**DiscoveryView**、**GrowthView** 等无需因本交付代码而改动。
- 调用方仍按现有方式传入 6 参(含 `navigationPath?`、`isLastNode`、`courseTitle`);完结页由 TabView 内嵌展示,不再依赖 push `.completion` 或占位页 `onChange`
---
## 六、审查结论汇总
| 项目 | 结论 |
|------|------|
| **isFirstNodeInChapter** | 已完全回滚为含 `.sorted { $0.order < $1.order }` 的写法,与当前仓库 100% 一致 ✅ |
| **loadMapData** | 无全局排序,与当前一致 ✅ |
| **CompletionView** | 仅 UI 更新,可全量替换 ✅ |
| **VerticalScreenPlayerView** | 仅替换主视图 Struct保留同文件其余代码逻辑与展示符合「逻辑回滚 + 统一分页」目标 ✅ |
| **其他页面** | 无需改动,零影响 ✅ |
**未对仓库内任何文件进行修改。**

View File

@ -1,73 +0,0 @@
# 完成页「最终交付 · 逻辑回滚」审查报告(禁止应用)
**审查对象**isFirstNodeInChapter 已恢复为含 `.sorted { $0.order < $1.order }` 的最终交付代码;确认不影响现有逻辑与展示。
**结论**:仅审查,不修改仓库内任何文件。
---
## 一、isFirstNodeInChapter 与当前仓库一致性
| 项目 | 当前仓库 | 本版交付 | 结论 |
|------|----------|----------|------|
| **排序** | `validNodes = chapter.nodes.filter(...).sorted { $0.order < $1.order }` | 同:`.filter { $0.status != .locked }.sorted { $0.order < $1.order }` | ✅ 一致 |
| **首节判定** | `validNodes.first?.id == nodeId`(在「包含 nodeId 的 chapter」内 | `validNodes.first?.id == nodeId`(遍历每章,等价语义) | ✅ 等价 |
本版写法:对每章取 `validNodes`filter + sort by order`validNodes.first?.id == nodeId` 则返回 true。
当前写法:先找到包含 nodeId 的 chapter再在该章内取 validNodesfilter + sort by order返回 `validNodes.first?.id == nodeId`
二者语义相同「nodeId 是否为其所在章内按 order 排序后的第一个未锁定节点」。
**结论**:章节判定逻辑与现有实现保持 100% 一致,未改动核心逻辑。
---
## 二、loadMapData无全局排序
- **本版**`realNodes = data.chapters.flatMap { $0.nodes }.filter { $0.status != .locked }`,无 `.sorted`
- **当前**`allCourseNodes = data.chapters.flatMap { $0.nodes }.filter { $0.status != .locked }`,无排序。
**结论**:保持「章节顺序 + 数组顺序」,未引入任何重排,与现有逻辑一致。
---
## 三、CompletionView
- **职责**:仅 UI粉紫勋章、共完成 N 节、底部「回到我的内容」)+ `navStore.switchToGrowthTab()`,无业务/数据逻辑。
- **接口**courseId / courseTitle / completedLessonCount 三参保留,与 CourseNavigation.completion 及三处 destination 兼容。
- **操作**:全量替换 `Views/CompletionView.swift` 即可。
**结论**:仅完结页 UI 更新,不影响现有逻辑与展示。
---
## 四、VerticalScreenPlayerView
### 4.1 已核对项
- **Init**6 参未改,外部调用零影响。
- **loadMapData**flatMap + filter无 sortallItems = realNodes.map(.lesson) + append(.completion)。
- **isFirstNodeInChapter**:含 `.sorted { $0.order < $1.order }`,与当前仓库等价。
- **getChapterTitle**:与当前一致。
- **currentPositionProgress**:仅按 lesson 项计算,忽略完结页。
- **handleBack / 错误态 / toast / hideTabBar / showTabBar**:均保留。
- **LessonPageView**:传 `self.courseTitle ?? mapData?.courseTitle`、navigationPathheaderConfig 仍用 isFirstNodeInChapter / getChapterTitle。
- **CompletionView内嵌**courseId、courseTitle、completedLessonCount。
### 4.2 合并范围(再次强调)
- **仅替换** `struct VerticalScreenPlayerView { ... }` 及内部的 `enum PlayerItem`
- **不得删除** 同文件内的 HeaderConfig、DuolingoProgressBar、CourseProgressNavBar、LessonPageView、LessonSkeletonView、LessonErrorView、CompletionPlaceholderPage或占位相关等其余类型。
**结论**:在仅替换主视图 Struct 的前提下,现有逻辑与展示不受影响。
---
## 五、审查结论汇总
| 项目 | 结论 |
|------|------|
| **isFirstNodeInChapter** | 已恢复为含 `.sorted { $0.order < $1.order }` 的写法,与当前仓库语义一致,未改动核心判定逻辑。 |
| **loadMapData** | 无全局排序,保持原有顺序。 |
| **CompletionView** | 仅 UI 更新,可全量替换。 |
| **VerticalScreenPlayerView** | 仅替换主视图 Struct保留同文件其余代码其他逻辑与展示不变。 |
| **现有逻辑与展示** | 在按上述范围替换的前提下,不会受到影响。 |
**未对仓库内任何文件进行修改。**

View File

@ -1,89 +0,0 @@
# 完成页「最终修正版」代码审查报告(禁止应用)
**审查对象**:基于零影响审查报告修正后的 CompletionView + VerticalScreenPlayerView 最终版(参数优先、错误 Toast、排序逻辑、接口兼容
**结论**:仅审查,不修改仓库内任何文件。核对三项修正与 6 条零影响条件,并说明应用时的合并范围。
---
## 一、三项修正落实情况
| 修正项 | 审查报告要求 | 本版代码 | 结论 |
|--------|----------------|----------|------|
| **参数优先权** | LessonPageView 传 `courseTitle ?? mapData?.courseTitle` | `courseTitle: self.courseTitle ?? mapData?.courseTitle`LessonPageView 与 CompletionView 均用) | ✅ 已落实 |
| **错误处理** | loadMapData 失败时 `showToastMessage("加载失败")` | catch 内 `self.showToastMessage("加载失败")` | ✅ 已落实 |
| **排序逻辑** | isFirstNodeInChapter 用 `validNodes.sorted { $0.order < $1.order }` 再取 first | `validNodes = chapter.nodes.filter(...).sorted { $0.order < $1.order }`,再 `validNodes.first?.id == nodeId` | ✅ 已落实 |
---
## 二、6 条零影响条件核对
| # | 条件 | 本版代码 | 结论 |
|---|------|----------|------|
| 1 | VerticalScreenPlayerView init 保持 6 参 | `init(courseId, nodeId, initialScrollIndex, navigationPath?, isLastNode?, courseTitle?)` 完整 | ✅ |
| 2 | 保留 CourseNavigation.completion 及三处 destination | 未改枚举与三 TabCompletionView 三参 | ✅ |
| 3 | CompletionView 保留三参 | courseId, courseTitle, completedLessonCount | ✅ |
| 4 | LessonPageView 传 courseTitle、navigationPath | `self.courseTitle ?? mapData?.courseTitle``navigationPath` 透传 | ✅ |
| 5 | 保留 loadError、toast、hideTabBar/showTabBar、handleBack(path 优先) | 错误态、toast、tabBar、handleBack 均保留loadMapData 失败有 showToastMessage | ✅ |
| 6 | NoteTreeView / NoteListView 不修改 | init 未改,笔记流仍只传 3 参 | ✅ |
---
## 三、CompletionView 审查
- **接口**courseId / courseTitle / completedLessonCount 全保留,与 CourseNavigation.completion 及三处 destination 一致。✅
- **内部**:纯 UI粉紫勋章+ handleReturnToRoot() → navStore.switchToGrowthTab(),无 navigationPath、无侧滑 Hack。✅
- **视觉**:顶部「已完成」、底部「回到我的内容」淡蓝、无返回按钮。✅
**结论**CompletionView 可直接全量替换 `Views/CompletionView.swift`
---
## 四、VerticalScreenPlayerView 审查与合并范围
### 4.1 本版已包含且正确的部分
- Init 6 参、PlayerItem 枚举、allItems 数据源、loadMapDatarealNodes + append .completion、currentPositionProgress仅 lesson、handleBack(path 优先)、hideTabBar/showTabBar、showToastMessage、错误态 UI、toast、isFirstNodeInChapter含 .sorted { $0.order < $1.order }、getChapterTitle。✅
### 4.2 应用时必须注意的合并范围(未应用,仅说明)
当前 **VerticalScreenPlayerView.swift** 文件中除 `VerticalScreenPlayerView` 结构体外,还包含:
- **HeaderConfig**、**DuolingoProgressBar**、**CourseProgressNavBar**(播放器依赖)
- **CompletionPlaceholderPage****LastPageSwipeModifier**(本方案中不再需要,可删除)
- **LessonPageView**、**LessonSkeletonView**、**LessonErrorView** 及后续所有类型与扩展
你提供的「最终修正版」只包含 **VerticalScreenPlayerView 结构体****PlayerItem** 枚举,未包含上述类型。因此:
- **不能**用该片段整文件覆盖 **VerticalScreenPlayerView.swift**,否则会删掉 HeaderConfig、CourseProgressNavBar、LessonPageView 等,导致编译失败。
- **正确做法**:在 **VerticalScreenPlayerView.swift** 内只做「局部替换」:
- 用本版的 **VerticalScreenPlayerView 结构体**(含 PlayerItem、body、loadMapData、handleBack、isFirstNodeInChapter 等)替换现有的 **VerticalScreenPlayerView** 结构体;
- 删除 **CompletionPlaceholderPage**(或占位页相关逻辑),不再添加 LastPageSwipeModifier
- **保留** 文件内 **HeaderConfig**、**DuolingoProgressBar**、**CourseProgressNavBar**、**LessonPageView**、**LessonSkeletonView**、**LessonErrorView** 及之后所有内容不变。
### 4.3 realNodes 的 .sorted 与当前行为差异(可选核对)
- **本版**`let realNodes = data.chapters.flatMap { $0.nodes }.filter { $0.status != .locked }.sorted { $0.order < $1.order }`
即对 **整课** 的节点列表按 `node.order` 做一次全局排序。
- **当前实现**`allCourseNodes = data.chapters.flatMap { $0.nodes }.filter { $0.status != .locked }`
**不** 对 flatMap 后的列表排序,顺序为:先 chapter 顺序,再各 chapter 内 nodes 的数组顺序。
若后端 `node.order`**按章节内** 的(例如每章都是 0,1,2,…),则对整课 flat 列表只按 `order` 排序会 **打乱章节顺序**(例如把各章 order=0 的节点排在一起)。若希望与当前行为完全一致,可考虑:
- 要么 **去掉** realNodes 的 `.sorted { $0.order < $1.order }`,保持与当前一致的「章节顺序 + 章内数组顺序」;
- 要么在确认后端 `order` 为全局唯一或全局有序的前提下保留当前排序。
是否保留该 sort可根据产品/后端约定决定;不影响三项修正与 6 条零影响条件。
---
## 五、审查结论汇总
| 项目 | 结论 |
|------|------|
| **三项修正** | 参数优先权、错误 Toast、isFirstNodeInChapter 排序均已落实。 |
| **6 条零影响** | 全部满足;对外接口与调用方无需改动。 |
| **CompletionView** | 可直接全量替换 `ios/WildGrowth/WildGrowth/Views/CompletionView.swift`。 |
| **VerticalScreenPlayerView** | 仅可替换 **结构体** 并删除占位页相关类型;必须保留同文件内 HeaderConfig、CourseProgressNavBar、LessonPageView 等所有其他类型,不能整文件覆盖。 |
| **realNodes 排序** | 与当前实现存在差异,是否保留 `.sorted { $0.order < $1.order }` 需结合后端 order 语义决定。 |
**未对仓库内任何文件进行修改。**

View File

@ -1,78 +0,0 @@
# CompletionView 粉紫掌机版 — 定稿审查报告(不应用)
**审查日期**2025-01-29
**范围**Pink-Purple Gameboy Edition赛博粉紫外壳 + 黑框大屏 + 蓝色胶囊按钮 + 数字滚动)
**结论**:仅审查、不修改仓库;逻辑继承正确,有一处待确认的 UI 细节。
---
## 1. 修改点落实情况
| 修改点 | 要求 | 定稿实现 | 结论 |
|--------|------|----------|------|
| **外壳颜色** | 赛博粉紫渐变 (Hot Pink → Deep Purple) | `shellGradient``Color(1, 0.35, 0.8)` → `Color(0.58, 0.2, 0.9)` | ✅ |
| **去除装饰** | 右下角勋章删掉,只留数据 | `Spacer()` 替代原勋章位,注释 "已删除勋章" | ✅ |
| **屏幕布局** | 严格复刻截图 | 上TOTAL → 数字 → LESSONSFOCUS TIME → 数字 min无 ONLINE/电量 | ✅ |
| **按钮** | 蓝色胶囊,与粉紫撞色 | 亮蓝渐变 + 凹槽 + 高光,保留撞色效果 | ✅ |
| **动效** | 点击 → 屏幕闪白 → 数字匀速滚动 | `performActivation``isSystemOn = true` + `withAnimation(.linear(duration: 1.5))` | ✅ |
---
## 2. 逻辑继承FRC 要求)
| 项目 | 要求 | 定稿实现 | 结论 |
|------|------|----------|------|
| **持久化 Key** | `has_revealed_course_\(courseId)` | 一致 | ✅ |
| **游客短路** | 不调网络,短延迟后直接激活 | `if userManager.isGuest { asyncAfter(0.5) { performActivation(0,0) }; return }` | ✅ |
| **游客延迟** | 极短假连接 | 0.5s | ✅ |
| **登录用户** | 拉取 fetchUserProfile 后激活 | `Task { sleep(0.8s); fetchUserProfile; performActivation(lessons, minutes) }` | ✅ |
---
## 3. RollingNumberText 与 Animatable
- `RollingNumberText` 正确实现 `Animatable``animatableData` 读写 `value``Double` 符合 `VectorArithmetic`,数字在 1.5s 内线性插值显示。
- `displayLessons`、`displayMinutes` 初始为 0激活时设为目标值配合 `withAnimation(.linear(duration: 1.5))` 实现匀速滚动。
---
## 4. checkSystemStatus 与数据来源
- 已激活时:`displayLessons`、`displayMinutes` 从 `userManager.studyStats` 取,保证二次进入时显示正确。
- 游客:`performActivation(0, 0)`,与未登录、无服务端统计一致。
- 登录用户:`performActivation(lessons, minutes)` 使用 `fetchUserProfile` 后的 `studyStats`,正确。
---
## 5. 待确认 UI 细节
**按钮未按下时的图标**:当前在 `!isBooting && !isSystemOn` 时显示 `Image(systemName: "checkmark")`,与激活后相同。若希望更易理解「待操作」,可改为 `"arrow.down.circle.fill"``"checkmark.circle"` 等,属可选优化,非阻塞。
---
## 6. 可选小修正(按需)
- **ForEach(0..<100)**:扫描线纹理的 `ForEach(0..<100)` 若在部分 Swift/SwiftUI 版本报错,可加上 `id: \.self`
---
## 7. 接口与影响范围
| 项目 | 结论 |
|------|------|
| **初始化参数** | 三参数不变,调用方无需修改 |
| **替换方式** | 直接整文件替换 `CompletionView.swift` |
| **其他页面** | 无影响 |
---
## 8. 总结
| 维度 | 结论 |
|------|------|
| **视觉修改** | ✅ 粉紫外壳、去勋章、复刻布局、蓝胶囊按钮、数字滚动 |
| **逻辑继承** | ✅ Key 回滚、游客短路、登录用户拉取逻辑一致 |
| **动效** | ✅ 屏幕闪白 + 1.5s 线性滚动 |
| **接口兼容** | ✅ 可直接替换 |
**审查结论**:定稿代码满足设计要求,逻辑继承正确,可直接用于替换。**本次未对仓库做任何修改。**

View File

@ -1,42 +0,0 @@
# 给 Gemini 的反馈:完结页不要改逻辑层
## 核心要求
**坚持「点击按钮时拉后端」**,不要改逻辑层。
---
## 1. 数据流(必须保持)
- **当前逻辑**:用户点击「标记完成」/「上传学习数据」等按钮 → **在 CompletionView 内部**调用后端拉取最新数据(如 `UserManager.shared.fetchUserProfile()`)→ 用拉取结果(如 `UserManager.shared.studyStats.lessons` / `.time`)展示。
- **要求**:完结页的**数据来源与拉取时机**保持上述逻辑,即**点击按钮时在页面内拉后端**,不在父视图提前传「已完成小节数、专注时长」等业务数据。
---
## 2. 不要做的改动(逻辑层)
- **不要**让 CompletionView 增加必选参数 `completedLessonCount`、`focusMinutes` 等由父视图传入的「业务数据」。
- **不要**把「拉取最新统计」的责任从 CompletionView 挪到父视图(如 VerticalScreenPlayerView或改成「进入页面时父视图传参」。
- **不要**改变「点击按钮 → 调接口 → 用接口/本地统计结果展示」这一套流程;可以保留「上传/显影」的**交互与动效**,但**显影后展示的数据必须来自点击后拉取的结果**(例如继续用 UserManager.studyStats 或等价数据源)。
---
## 3. 可以做的(仅 UI / 视觉)
- 可以改完结页的**视觉与动效**(例如拍立得、赛博海报、骨架、显影动画等)。
- 可以改**按钮文案**(如「上传学习数据」「标记完成」)和**排版**(顶对齐、大数字等)。
- 可以保留 **UserDefaults 显影状态**(已显影过则直接展示结果态,不重复播动画),只要结果态展示的数据仍来自**点击时拉取的后端/统计**(例如首次显影时点按钮拉取,之后用本地缓存或同一数据源)。
- **回到我的内容**:必须调用 `navStore.switchToGrowthTab()`,回到技能 Tab。
---
## 4. 接口约定(建议)
- CompletionView 的入参保持与现有一致或仅做**可选**扩展(如可选 `navigationPath`**不要**新增必选的「业务数据」参数(如 `completedLessonCount: Int`、`focusMinutes: Int`)。
- 小节数、专注时长等**在 CompletionView 内部**从现有数据源获取(如 UserManager.studyStats并在**用户点击按钮后**通过现有或约定的接口拉取最新再展示。
---
## 5. 一句话总结
**只改完结页的 UI/动效/文案,不要改「点击按钮时拉后端、用拉取结果展示」的逻辑与数据层;不要通过父视图传入 completedLessonCount、focusMinutes 等业务数据。**

View File

@ -1,122 +0,0 @@
# 完成页「统一分页架构」— 零影响审查报告(禁止应用)
**审查目标**:若应用此方案,确保**其他页面、其他功能、其他逻辑一点也不受影响**。
**结论**:仅审查,不修改仓库内任何文件。下文列出所有受影响点及达成「零影响」的**必要条件**。
---
## 一、方案会动到的文件与接口
| 文件 | 方案中的改动 |
|------|----------------|
| **CompletionView.swift** | 整文件替换:去掉 courseId仅保留 courseTitle、completedLessonCount纯 UI + switchToGrowthTab |
| **VerticalScreenPlayerView.swift** | 数据源改为 allItems (PlayerItem);去掉占位页与 onChangeinit 去掉 navigationPath、isLastNode、courseTitlehandleBack 仅 dismiss()LessonPageView 传参可能变 |
---
## 二、依赖关系与「零影响」条件
### 2.1 谁调用 VerticalScreenPlayerView
| 调用方 | 当前传参 | 方案中 init 若改为 3 参会怎样 |
|--------|----------|------------------------------|
| **GrowthView** (.player) | courseId, nodeId, initialScrollIndex: nil, **navigationPath: $growthPath**, **isLastNode**, **courseTitle** | ❌ 编译失败(多传 3 个参数) |
| **ProfileView** (.player) | 同上navigationPath: $profilePath | ❌ 同上 |
| **DiscoveryView** (.player) | 同上navigationPath: $homePath | ❌ 同上 |
| **NoteTreeView** (.player) | courseId, nodeId, initialScrollIndex无 path / isLastNode / courseTitle | ✅ 仍兼容 3 参 init |
**零影响条件 1**
**VerticalScreenPlayerView 的 init 必须保留现有 6 参签名**courseId, nodeId, initialScrollIndex, navigationPath?, isLastNode?, courseTitle?),且默认值不变。内部可改用 allItems + PlayerItem但对外接口不变这样 GrowthView / ProfileView / DiscoveryView **无需改一行**
---
### 2.2 谁使用 CourseNavigation 与 CompletionView
| 位置 | 当前行为 | 方案若不再 push .completion 会怎样 |
|------|----------|-------------------------------------|
| **GrowthView** | navigationDestination(.completion) → CompletionView(courseId, courseTitle, completedLessonCount) | 从「课程流」进完成页时不再走此分支(完成页在 TabView 内);若保留此分支,深链/通知若 push .completion 仍能展示 |
| **ProfileView** | 同上 | 同上 |
| **DiscoveryView** | 同上 | 同上 |
| **MapView** | 只 append .player不直接 append .completion | ✅ 无影响 |
| **VerticalScreenPlayerView**(当前) | 滑到占位页时 append .completion | 方案中不再 append完成页在 TabView 内 |
**零影响条件 2**
- **保留** `CourseNavigation.completion` 与三处 `.completion` 的 navigationDestination。
- **CompletionView 仍保留三参**courseId, courseTitle, completedLessonCount。这样
- 从课程流进入时,完成页由播放器内 TabView 展示,不经过 navigationDestination
- 若有深链/通知/其他入口直接 push .completion三处 destination 仍能正确展示 CompletionView且接口一致。
---
### 2.3 LessonPageView 的传参
| 当前传参 | 方案中若改为不传 navigationPath / 不传 courseTitle 会怎样 |
|----------|-----------------------------------------------------------|
| courseId, nodeId, currentGlobalNodeId, initialScrollIndex, headerConfig, **courseTitle**, **navigationPath** | LessonPageView 中两者均为可选;不传则 nil。若内部有逻辑依赖 path 或 courseTitle可能受影响。 |
**零影响条件 3**
在「统一分页」的播放器内部,对 **LessonPageView** 的调用仍传入 **courseTitle**、**navigationPath**(用 VerticalScreenPlayerView 持有的 courseTitle、navigationPath与当前一致。即仅改数据源与最后一页渲染方式不减少对 LessonPageView 的传参。
---
### 2.4 播放器内部逻辑(其他功能)
| 当前能力 | 方案中若省略会怎样 |
|----------|--------------------|
| **handleBack** 用 navigationPath.removeLast() 或 dismiss() | 若改为仅 dismiss(),在 NavigationStack 内效果通常等价pop 一层)。为保险起见,建议保留:有 path 且非空时 removeLast(),否则 dismiss()。 |
| **loadError / 错误态 UI** | 方案片段中未写,若删除则错误态消失。 |
| **showToast / toastMessage** | 同上,若删除则 toast 能力消失。 |
| **hideTabBar / showTabBaronAppear / onDisappear** | 若删除则播放器出现时 TabBar 可能仍显示。 |
| **currentPositionProgress**(不含占位页) | 方案中有「进度计算忽略完结页」,逻辑等价即可。 |
| **GeometryReader 等布局** | 若删除可能影响布局,建议保留与当前一致。 |
**零影响条件 4**
播放器内**保留**loadError / 错误态 UI、showToast / toastMessage、hideTabBar / showTabBar、handleBack 的 path 优先逻辑(若保留 navigationPath 参数则一并保留)、以及现有布局与进度计算方式(仅把「占位页」换成虚拟 completion 页,进度仍只按真实小节算)。
---
### 2.5 笔记流NoteTreeView / NoteListView
| 当前 | 说明 |
|------|------|
| 使用 NoteNavigationDestination.player传 courseId, nodeId, initialScrollIndex | 不传 navigationPath、isLastNode、courseTitle当前 init 中这些为可选且带默认值。 |
**零影响条件 5**
保持 VerticalScreenPlayerView 的 init 中 **navigationPath、isLastNode、courseTitle 为可选且默认 nil**。这样 NoteTreeView / NoteListView **无需任何修改**,行为不变(不传 path 则不会 push 完成页;统一分页下完成页在 TabView 内,笔记流仍不涉及 .completion 路由)。
---
### 2.6 其他引用
| 引用 | 影响 |
|------|------|
| **ContentBlockBuilder** 注释「与 VerticalScreenPlayerView 保持一致」 | 仅注释,不依赖接口或类型。 |
| **CourseNavigation** 枚举 | 保留 .map / .player / .completion 三 case见零影响条件 2。 |
| **MapView** 中 append CourseNavigation.player(courseId, nodeId, isLastNode, courseTitle) | 不依赖播放器 init 是否接收这些参数,只要枚举不变即无影响。 |
---
## 三、零影响检查清单(应用前必达)
若要在「其他页面、其他功能、其他逻辑一点也不受影响」的前提下应用统一分页方案,需同时满足:
| # | 条件 | 说明 |
|---|------|------|
| 1 | VerticalScreenPlayerView **init 保持 6 参**courseId, nodeId, initialScrollIndex, navigationPath?, isLastNode?, courseTitle? | GrowthView / ProfileView / DiscoveryView 不改动 |
| 2 | 保留 **CourseNavigation.completion** 及三处 **.completion** 的 navigationDestination | 深链/其他入口 push .completion 仍可用 |
| 3 | **CompletionView 保留三参**courseId, courseTitle, completedLessonCount | 与现有 destination 及枚举一致 |
| 4 | 播放器内对 **LessonPageView** 仍传 **courseTitle**、**navigationPath**(用播放器自身属性) | LessonPageView 行为不变 |
| 5 | 播放器内保留 **loadError、toast、hideTabBar/showTabBar、handleBack(path 优先)** 及现有布局/进度逻辑 | 错误、提示、TabBar、返回、进度均不受影响 |
| 6 | **NoteTreeView / NoteListView** 不修改 | 依赖 init 可选参数,已满足则零影响 |
---
## 四、总结
- **按方案原文直接替换**init 改为 3 参、CompletionView 去掉 courseId、播放器去掉 path/错误/toast/tabBar 等):**会**影响 GrowthView / ProfileView / DiscoveryView编译或行为、以及错误态/toast/TabBar/返回等逻辑。
- **在满足上述 6 条零影响条件的前提下**,再引入 allItems + PlayerItem、TabView 最后一页渲染 CompletionView、不再 push .completion
- 其他页面(含三 Tab、MapView、笔记流**无需改**
- 其他功能错误、toast、TabBar、返回、进度、深链 .completion**不受影响**
- 其他逻辑完课统计、LessonPageView、CourseNavigation**保持一致**。
**未对仓库内任何文件进行修改。**

View File

@ -1,97 +0,0 @@
# 完成页「统一分页 + 零影响」代码审查报告(禁止应用)
**审查对象**:严格遵守 6 条零影响条件的 CompletionView + VerticalScreenPlayerView 完整代码(内部重构,外部兼容)。
**结论**:仅审查,不修改仓库内任何文件。对照 6 条逐项核对,并列出需补全/修正的细节以达「绝对零影响」。
---
## 一、6 条零影响条件对照
| # | 条件 | 本版代码 | 结论 |
|---|------|----------|------|
| 1 | VerticalScreenPlayerView init 保持 6 参 | `init(courseId, nodeId, initialScrollIndex, navigationPath?, isLastNode?, courseTitle?)` 完整保留 | ✅ 满足 |
| 2 | 保留 CourseNavigation.completion 及三处 destination | 未改枚举与三 TabCompletionView 三参destination 调用不变 | ✅ 满足 |
| 3 | CompletionView 保留三参 | `courseId`, `courseTitle`, `completedLessonCount` 均有 | ✅ 满足 |
| 4 | LessonPageView 仍传 courseTitle、navigationPath | 传 `courseTitle: mapData?.courseTitle`、`navigationPath: navigationPath` | ⚠️ 见下 4.1 |
| 5 | 保留 loadError、toast、hideTabBar/showTabBar、handleBack(path 优先) | 错误态、toast、tabBar、handleBack 均保留 | ⚠️ 见下 5.1、5.2 |
| 6 | NoteTreeView / NoteListView 不修改 | init 未改,笔记流仍只传 3 参 | ✅ 满足 |
---
## 二、CompletionView 审查
- **接口**courseId / courseTitle / completedLessonCount 全保留,与 CourseNavigation.completion 及三处 destination 一致。✅
- **内部**:纯 UI粉紫勋章+ navStore.switchToGrowthTab(),无 navigationPath、无侧滑 Hack。✅
- **视觉**:顶部「已完成」、底部「回到我的内容」淡蓝、无返回按钮。与需求一致。✅
- **无遗漏**:无依赖 dismiss、无依赖 path作为 TabView 一页或作为 push 目标均可。✅
**结论**CompletionView 满足零影响条件,无需改动。
---
## 三、VerticalScreenPlayerView 审查
### 3.1 已满足项
- **Init**6 参完整,与现有 GrowthView / ProfileView / DiscoveryView / NoteTreeView / NoteListView 调用兼容。✅
- **PlayerItem**lesson(MapNode) + completionid 分别为 node.id 与 `"COMPLETION_PAGE"`。✅
- **loadMapData**realNodes + append(.completion)allItems 构造正确loadError = nil 与 catch 内 set loadError 均有。✅
- **currentPositionProgress**:仅用 lesson 项计算,完结页时返回 1.0,逻辑正确。✅
- **handleBack**path 非空则 removeLast(),否则 dismiss()。✅
- **hideTabBar / showTabBar / toast**:保留。✅
- **LessonPageView**:传 courseId, nodeId, currentGlobalNodeId, initialScrollIndex, headerConfig, courseTitle, navigationPath。✅
- **CompletionView内嵌**:传 courseId, courseTitle, completedLessonCount。✅
### 3.2 需补全或修正的细节(达「绝对零影响」)
#### 4.1 LessonPageView 的 courseTitle 传参(对应条件 4
- **本版**`courseTitle: mapData?.courseTitle`(仅用加载后的 mapData
- **当前实现**`courseTitle: courseTitle`(用调用方传入的 courseTitle如 MapView 的 data.courseTitle
- **差异**:加载完成前 mapData 为 nil本版会传 nil当前实现一进入就有值。若希望与现有行为完全一致建议传 **`courseTitle ?? mapData?.courseTitle`**,优先用传入值,再回退到加载结果。
#### 5.1 loadMapData 失败时的 Toast对应条件 5
- **当前实现**catch 中除 set loadError 外,还调用 `showToastMessage("加载失败")`
- **本版**catch 中只 set loadError未调用 showToastMessage。
- **建议**:在 catch 的 MainActor.run 内补上 **`showToastMessage("加载失败")`**,与现有体验一致。
#### 5.2 isFirstNodeInChapter 实现(对应条件 5逻辑不变
- **当前实现**:在 chapter 内取 `validNodes = chapter.nodes.filter(locked).sorted { $0.order < $1.order }`,再 `validNodes.first?.id == nodeId`(即按 **order** 排序后的「第一章第一节」)。
- **本版**`chapter.nodes.filter(locked).first`,未按 order 排序,相当于用数组顺序的「第一个」。
- **风险**:若后端/本地 chapter.nodes 顺序与 order 不一致,本版可能与当前表现不同。
- **建议**:与当前保持一致,使用 **`validNodes = chapter.nodes.filter { $0.status != .locked }.sorted { $0.order < $1.order }`,再 `validNodes.first?.id == nodeId`**。
#### 5.3 其他可选一致性(非必须)
- **空状态文案**:当前为「暂无内容」+「该课程还没有可用的学习内容」;本版为「暂无内容」+ 单行。若需完全一致可补副标题,否则可保留本版简化。
- **GeometryReader**:当前 body 最外层包了一层 GeometryReader本版未包。若当前无依赖 geo 的布局,可不再加;若有,需保留或等价处理。
- **Import**hideTabBar/showTabBar 使用 UIApplication**import UIKit**。若文件当前已含 UIKit 则无需改;否则需补。
---
## 四、对外暴露与调用方
| 调用方 / 入口 | 是否需改 | 说明 |
|---------------|----------|------|
| GrowthView / ProfileView / DiscoveryView.player / .completion | 否 | init 与 CompletionView 三参未变 |
| MapViewappend .player | 否 | 枚举与参数不变 |
| NoteTreeView / NoteListView.player | 否 | 仍只传 3 参,可选参默认 nil |
| CourseNavigation 枚举 | 否 | 未改 |
| navigationDestination(.completion) | 否 | 仍用 CompletionView(courseId, courseTitle, completedLessonCount) |
**结论**:在补全 4.1、5.1、5.2 后,对外接口与所有调用方均可保持零改动、零行为差异。
---
## 五、审查结论汇总
| 项目 | 结论 |
|------|------|
| **6 条零影响** | 条件 1、2、3、6 已满足;条件 4、5 在按 3.2 补全后可达「绝对零影响」。 |
| **CompletionView** | 可直接采用,无需改。 |
| **VerticalScreenPlayerView** | 建议补全:① LessonPageView 传 `courseTitle ?? mapData?.courseTitle`;② loadMapData 失败时 `showToastMessage("加载失败")`;③ isFirstNodeInChapter 按 order 排序取 first。 |
| **其他页面 / 功能 / 逻辑** | 在以上补全前提下,其他页面、其他功能、其他逻辑均不受影响。 |
**未对仓库内任何文件进行修改。**

View File

@ -1,64 +0,0 @@
# 完成页「统一分页架构」— 审查报告(禁止应用)
**审查对象**:虚拟节点 (PlayerItem.completion) 作为 TabView 最后一页CompletionView 内嵌于播放器;是否导致「完成小节统计虚高」。
**结论**:仅审查,不修改仓库内任何文件。
---
## 一、核心问题:会不会导致完成小节统计虚高?
**结论:不会。**
### 1.1 数据流梳理
| 环节 | 来源 / 行为 | 是否计入「完成小节」 |
|------|-------------|----------------------|
| **展示数字** | CompletionView 显示的 `completedLessonCount` 来自 `UserManager.shared.studyStats.lessons` | 仅展示,不写入 |
| **统计来源** | `studyStats.lessons` 由后端/用户接口(如 `completed_lessons`)拉取并写入 UserManager | 后端口径为真实完课数 |
| **完课上报** | 仅在 **LessonPageView** 内,当该节进度 ≥ 1.0 时调用 `LearningService.shared.completeLesson(nodeId: ...)` | 只对**真实课程节点**上报 |
| **虚拟节点** | `PlayerItem.completion` 仅存在于前端的 `allItems`id 为 `"COMPLETION_PAGE"` | 不参与任何完课 API无 nodeId |
### 1.2 为何不会虚高
- **总结页不是一节课**:虚拟节点只用于 TabView 的「最后一页」展示,没有对应的 `MapNode`,也不会调用 `completeLesson(nodeId:)`
- **完课只发生在 LessonPageView**:只有渲染 `PlayerItem.lesson(node)` 时才会加载该节的进度、在达到条件时上报完课;渲染 `PlayerItem.completion` 时只渲染 CompletionView没有任何完课或统计写入逻辑。
- **展示用既有统计**CompletionView 只是读取并展示 `UserManager.shared.studyStats.lessons`,不修改该值;该值的增加只来自真实小节在 LessonPageView 中的完课上报与后端同步。
因此:**把总结页当作 TabView 最后一页、用虚拟节点渲染 CompletionView不会把总结页算成一节也不会导致完成小节统计虚高。**
---
## 二、方案本身审查(与当前实现对比)
### 2.1 优点
- **交互统一**:左滑/右滑完全由 TabViewUIScrollView负责无需自定义 DragGesture、无需 SwipeBackEnabler。
- **CompletionView 职责单一**:只做 UI 与「回到技能页根」按钮,不碰 navigationPath、不碰侧滑 Hack。
- **状态集中**:完结页是 `PlayerItem` 枚举的一支,与课程节点同属一个数据源,逻辑清晰。
### 2.2 需注意的改动(若将来应用)
1. **VerticalScreenPlayerView 的 init**
方案中去掉了 `navigationPath`、`isLastNode`、`courseTitle`。当前 **GrowthView / ProfileView / DiscoveryView**`.player` 分支会传这三个参数;若直接按方案改 init调用方会报错。需要二选一
- 保留这三个参数(兼容现有调用),或
- 同时改三处调用方,不再传这些参数。
2. **CompletionView 的接口**
方案中 CompletionView 只有 `courseTitle``completedLessonCount`(无 `courseId`)。若项目中别处仍通过 `CourseNavigation.completion(courseId: ...)` 等 push 进 CompletionView则需保留 `courseId` 或同步改那些入口。
3. **进度条与 TabBar**
当前实现有 `currentPositionProgress`不含占位页、hideTabBar/showTabBar、loadError、toast 等。方案里进度条在 `currentNodeId == "COMPLETION_PAGE"` 时隐藏其余若省略需确认是否要保留如错误态、toast、tabBar 隐藏)。
4. **笔记流**
NoteTreeView / NoteListView 里用的 VerticalScreenPlayerView 目前传的是 `NoteNavigationDestination.player`,不传 navigationPath。若播放器 init 去掉 navigationPath笔记流不受影响若保留可选 navigationPath 以兼容,也需在方案里写明。
---
## 三、审查结论汇总
| 问题 | 结论 |
|------|------|
| **会不会导致完成小节统计虚高?** | **不会**。总结页只是虚拟的一页,用于展示;完课统计只由 LessonPageView 对真实节点调用 completeLesson 产生CompletionView 仅读取 studyStats.lessons 做展示。 |
| 统一分页架构本身 | 交互简单、无需手势与侧滑 HackCompletionView 可做纯 UI若应用需处理 init 与调用方兼容、以及错误/toast/tabBar 等现有能力是否保留。 |
**未对仓库内任何文件进行修改。**

View File

@ -1,55 +0,0 @@
# 「赛博拍立得 Final」版 CompletionView 审查报告(禁止应用)
**审查对象**Gemini 提供的 Cyber Polaroid Final 版 CompletionView拍立得隐喻、UserDefaults 显影状态、navStore.switchToGrowthTab()、真实数据由参数传入)。
**结论**:仅审查,**不应用、不修改**仓库内任何文件。
---
## 一、需求符合性
| 项目 | 需求/说明 | 本版实现 | 结论 |
|------|-----------|----------|------|
| **回到我的内容** | 必须调用 `navStore.switchToGrowthTab()`,回到技能 Tab | `handleBackToContent()` 内先 `navStore.switchToGrowthTab()`,再清 path 或 dismiss | ✅ 符合 |
| **真实数据** | 小节数、专注时长由父视图传入,不写死 | `completedLessonCount`、`focusMinutes` 均为 `let`,由调用方传入 | ✅ 符合 |
| **状态持久化** | 用 UserDefaults 记录「该课程已显影」 | `storageKey = "has_revealed_course_\(courseId)"`,显影后 `set(true)`onAppear 时 `checkDevelopmentStatus()` | ✅ 符合 |
| **视觉与交互** | 未显影 → 上传/显影 → 赛博海报;顶对齐、大数字 | UndevelopedFilm + DevelopedPoster排版与说明一致 | ✅ 符合 |
---
## 二、接口变更与对「其他页面」的影响
| 项目 | 说明 |
|------|------|
| **CompletionView 入参** | 本版为 **必选**`courseId`, `courseTitle`, `completedLessonCount`, `focusMinutes`**可选**`navigationPath`。 |
| **当前调用处** | 仓库内仅 **VerticalScreenPlayerView** 一处构造 CompletionView当前传参为`courseId`, `courseTitle`, `navigationPath`**未传** `completedLessonCount`、`focusMinutes`)。 |
| **影响** | 若只替换 CompletionView.swift 而**不修改调用方**,会因缺少 `completedLessonCount`、`focusMinutes` 两个必选参数而**编译失败**。 |
因此:**必须同时修改 VerticalScreenPlayerView** 中构造 CompletionView 的那一行,补上:
- `completedLessonCount: UserManager.shared.studyStats.lessons`(或你项目里等价的数据源)
- `focusMinutes: UserManager.shared.studyStats.time`(或 0若暂无专注时长
**其他页面**:当前无其它地方使用 CompletionViewCourseNavigation 也无 `.completion` case故除 VerticalScreenPlayerView 这一处调用外,**无需改其它页面**;逻辑与展示也不受影响。
---
## 三、小结:是否「只动完结页」
| 维度 | 结论 |
|------|------|
| **仅替换 CompletionView.swift** | ❌ 不够。本版多了两个必选参数,**必须**在 VerticalScreenPlayerView 中补传 `completedLessonCount``focusMinutes`,否则无法编译。 |
| **替换 CompletionView + 修改 VerticalScreenPlayerView 内一行调用** | ✅ 可做到。其它页面GrowthView / ProfileView / MapView / CourseNavigation 等)逻辑与展示均不受影响。 |
| **若希望零改动调用方** | 可将 `focusMinutes` 改为带默认值,例如 `focusMinutes: Int = 0``completedLessonCount` 若希望与现有「由父视图传入」一致,建议保留必选并由 VerticalScreenPlayerView 传入。 |
---
## 四、审查结论汇总
| 项目 | 结论 |
|------|------|
| **需求** | 回到技能 Tab、真实数据由参数传入、UserDefaults 持久化、拍立得交互与排版均符合说明。 |
| **接口** | 新增必选参数 `completedLessonCount`、`focusMinutes`**会**影响当前唯一调用方 VerticalScreenPlayerView需补参。 |
| **其他页面** | 仅 VerticalScreenPlayerView 需改一行调用;其余页面与逻辑、展示均不受影响。 |
| **建议** | 不应用本报告所述代码;若采用本版,需同步在 VerticalScreenPlayerView 中为 CompletionView 传入 `completedLessonCount``focusMinutes`。 |
**未对仓库内任何文件进行修改。**

View File

@ -1,51 +0,0 @@
# 「赛博拍立得」版 CompletionView 审查报告(禁止应用)
**审查对象**Gemini 提供的 Cyber Polaroid 版 CompletionView拍立得隐喻、上传学习数据、hasUploadedData 状态持久化)。
**结论**:仅审查,**不应用、不修改**仓库内任何文件。
---
## 一、与当前需求的偏差
| 项目 | 当前需求 / 现有约定 | Gemini 本版实现 | 结论 |
|------|---------------------|-----------------|------|
| **底部按钮语义** | 点击「回到我的内容」应**回到技能 Tab我的课程列表**,即 `navStore.switchToGrowthTab()` | `handlePopToRoot()` 仅做 `path.wrappedValue = NavigationPath()``dismiss()`**未切到技能 Tab** | ❌ 行为不符:从发现/个人 Tab 进入完结页时,点按钮只会清栈或 dismiss仍停留在当前 Tab |
| **数据来源** | 共完成小节数来自接口/本地统计(如 `UserManager.shared.studyStats.lessons`),仅展示 | 新增 `focusMinutes: Int = 45` 写死;显影态展示「专注时长 (MIN)」,数据非来自现有统计接口 | ⚠️ 若产品无「专注时长」需求,属多余展示;若有,需接真实数据源 |
| **完结页入口** | 作为 TabView 最后一页内嵌,无「上传」步骤,仅展示 + 回到技能 Tab | 未显影 → 点击「上传学习数据」→ 模拟 1.5s 显影 → 显影态;依赖 `hasUploadedData` 状态 | ❌ 交互模型与当前「统一分页 + 纯展示」不一致,易与真实学习数据上报逻辑混淆 |
---
## 二、接口与调用方影响
| 项目 | 说明 |
|------|------|
| **CompletionView 入参** | Gemini 版为 `courseId, courseTitle, completedLessonCount, focusMinutes=45, navigationPath?`。当前工程若为 `courseId, courseTitle, navigationPath` 三参,需在调用处补传 `completedLessonCount`(如 `UserManager.shared.studyStats.lessons`)。 |
| **VerticalScreenPlayerView** | 若已按此前约定传 `CompletionView(courseId:courseTitle:completedLessonCount:navigationPath:)`,则参数兼容;**无需改其他页面文件**。 |
| **GrowthView / ProfileView / DiscoveryView** | 其 `navigationDestination(for: .completion)` 仍为 `CompletionView(courseId:courseTitle:completedLessonCount:)`;若 Gemini 版增加可选 `navigationPath`,调用处可不传或传入对应 path**其他页面逻辑与展示不受影响**。 |
结论:**仅替换 CompletionView 时,其他页面无需改代码即可编译**;但完结页**自身行为**会变(见上表)。
---
## 三、是否影响其他页面的逻辑与展示
| 维度 | 结论 |
|------|------|
| **其他页面逻辑** | 不受影响。仅 CompletionView 内部实现与状态变化。 |
| **其他页面展示** | 不受影响。无改动其他 View 或导航结构。 |
| **完结页自身逻辑与展示** | **会变**:从「勋章 + 点击点亮 + 回到我的内容 → 技能 Tab」变为「拍立得未显影 → 上传数据 → 显影 + 专注时长 + 回到我的内容 → 清栈/dismiss」且**不切技能 Tab**。 |
因此:**其他页面逻辑和展示不受影响****完结页的交互与目标(回到技能 Tab会受影响**,需按需求修正。
---
## 四、审查结论汇总
| 项目 | 结论 |
|------|------|
| **其他页面** | 逻辑与展示均不受影响;仅替换 CompletionView 时调用方可不改或仅补参。 |
| **底部按钮** | 未调用 `navStore.switchToGrowthTab()`,不符合「回到技能 Tab-我的课程列表」需求,需补回。 |
| **数据与交互** | 写死 focusMinutes、上传模拟、hasUploadedData 与当前「仅展示接口/本地统计 + 统一分页」不一致;若采用本版,需与产品/接口对齐并接入真实数据。 |
| **建议** | 不应用本版;若保留「赛博拍立得」视觉,需在不动其他页面的前提下:① 底部按钮改为 `navStore.switchToGrowthTab()`;② 移除或对接「上传学习数据」与「专注时长」逻辑,与现有统计与导航一致。 |
**未对仓库内任何文件进行修改。**

View File

@ -1,288 +0,0 @@
# 完成页迭代 — 完整代码(仅交付,禁止自动应用)
以下为审查修正后的完整代码。**关键修正**:底部「回到我的内容」改为 `navStore.switchToGrowthTab()`,不再使用 `navigationPath = NavigationPath()`,以保证从任意 Tab 进入都能回到技能页 Tab。
---
## 1. CompletionView.swift整文件替换
```swift
import SwiftUI
// MARK: - 🏆 课程完结页 (Gesture Flow Edition)
// 交互:左滑进入(上级控制),右滑返回(系统原生),底部点击回技能页 Tab
struct CompletionView: View {
let courseId: String
let courseTitle: String?
let completedLessonCount: Int
@EnvironmentObject private var navStore: NavigationStore
@Environment(\.dismiss) private var dismiss
@State private var isSealed = false
@State private var breathingOpacity: Double = 0.3
@State private var contentOpacity: Double = 0
var body: some View {
ZStack {
Color.bgPaper.ignoresSafeArea()
VStack(spacing: 0) {
Spacer().frame(height: 60)
Spacer()
ZStack {
Circle()
.strokeBorder(
isSealed ?
AnyShapeStyle(
LinearGradient(
colors: [Color.brandVital, Color.cyberIris],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
) :
AnyShapeStyle(Color.inkSecondary.opacity(0.3)),
style: StrokeStyle(lineWidth: isSealed ? 4 : 1, dash: isSealed ? [] : [5, 5])
)
.frame(width: 220, height: 220)
.shadow(
color: isSealed ? Color.brandVital.opacity(0.4) : .clear,
radius: 20, x: 0, y: 0
)
.scaleEffect(isSealed ? 1.0 : 0.95)
.opacity(isSealed ? 1.0 : breathingOpacity)
.animation(.easeInOut(duration: 0.5), value: isSealed)
if isSealed {
VStack(spacing: 8) {
Text("已完成的")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.inkSecondary)
Text("第 \(completedLessonCount) 节")
.font(.system(size: 36, weight: .bold, design: .serif))
.foregroundStyle(
LinearGradient(
colors: [Color.brandVital, Color.cyberIris],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
}
.transition(.scale(scale: 0.5).combined(with: .opacity))
} else {
Text("完")
.font(.system(size: 80, weight: .light, design: .serif))
.foregroundColor(.inkPrimary.opacity(0.2))
.opacity(breathingOpacity)
}
}
.contentShape(Circle())
.onTapGesture {
triggerInteraction()
}
Spacer()
Button {
handleReturnToRoot()
} label: {
HStack(spacing: 4) {
Text("回到我的内容")
.font(.system(size: 15, weight: .medium))
Image(systemName: "arrow.right")
.font(.system(size: 12))
}
.foregroundColor(.brandVital)
.padding(.vertical, 12)
.padding(.horizontal, 32)
.background(Capsule().fill(Color.brandVital.opacity(0.05)))
}
.opacity(contentOpacity)
.padding(.bottom, 80)
}
}
.toolbar(.hidden, for: .navigationBar)
.modifier(SwipeBackEnablerModifier())
.onAppear {
startBreathing()
}
}
private func startBreathing() {
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) {
breathingOpacity = 0.8
}
}
private func triggerInteraction() {
guard !isSealed else { return }
let generator = UIImpactFeedbackGenerator(style: .heavy)
generator.impactOccurred()
withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
isSealed = true
}
withAnimation(.easeOut.delay(0.5)) {
contentOpacity = 1.0
}
}
/// 回到技能页 Tab 根目录(与当前「继续学习」一致)
private func handleReturnToRoot() {
navStore.switchToGrowthTab()
}
}
// MARK: - 侧滑返回:隐藏导航栏时仍可右滑 pop
struct SwipeBackEnablerModifier: ViewModifier {
func body(content: Content) -> some View {
content.background(SwipeBackEnabler())
}
}
private struct SwipeBackEnabler: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController { UIViewController() }
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
DispatchQueue.main.async {
if let nc = uiViewController.navigationController {
nc.interactivePopGestureRecognizer?.delegate = nil
nc.interactivePopGestureRecognizer?.isEnabled = true
}
}
}
}
```
---
## 2. VerticalScreenPlayerView.swift仅改动部分
### 2.1 保持现有 init 与属性
- `navigationPath: Binding<NavigationPath>?` 保持可选NoteTreeView / NoteListView 不传时仍不 push 完成页。
- `isLastNode`, `courseTitle` 等保持不变。
### 2.2 替换「加载成功」分支:去掉占位页,仅用左滑手势 push
**原逻辑**(约 146186 行):
`TabView``ForEach(allCourseNodes)` + `CompletionPlaceholderPage().tag("wg://completion")`,外加 `.onChange(of: currentNodeId)``newId == "wg://completion"` 时 append completion。
**新逻辑**
- `TabView` 内**仅** `ForEach(allCourseNodes)`,每页在最后一节上挂 `LastPageSwipeModifier`,左滑时调用 `triggerCompletionNavigation`
- **删除** `CompletionPlaceholderPage` 及其 tag、**删除** `.onChange(of: currentNodeId)` 中与 `"wg://completion"` 相关的逻辑。
- 新增 `@State private var isNavigatingToCompletion = false`(防抖),以及 `triggerCompletionNavigation()`、`LastPageSwipeModifier`。
**替换后的内容区代码**(直接替换原 `else if !allCourseNodes.isEmpty { ... }` 整块):
```swift
} else if !allCourseNodes.isEmpty {
TabView(selection: $currentNodeId) {
ForEach(allCourseNodes, id: \.id) { node in
let isFirst = isFirstNodeInChapter(nodeId: node.id)
let chapterTitle = getChapterTitle(for: node.id)
LessonPageView(
courseId: courseId,
nodeId: node.id,
currentGlobalNodeId: $currentNodeId,
initialScrollIndex: node.id == initialNodeId ? initialScrollIndex : nil,
headerConfig: HeaderConfig(
showChapterTitle: isFirst,
chapterTitle: chapterTitle
),
courseTitle: courseTitle,
navigationPath: navigationPath
)
.tag(node.id)
.modifier(LastPageSwipeModifier(
isLastPage: node.id == allCourseNodes.last?.id,
onSwipeLeft: triggerCompletionNavigation
))
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.ignoresSafeArea(.container, edges: .bottom)
}
```
### 2.3 新增状态与函数(放在 Logic Helpers 区域)
```swift
@State private var isNavigatingToCompletion = false
private func triggerCompletionNavigation() {
guard !isNavigatingToCompletion, let path = navigationPath else { return }
isNavigatingToCompletion = true
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred()
let count = UserManager.shared.studyStats.lessons
path.wrappedValue.append(CourseNavigation.completion(
courseId: courseId,
courseTitle: courseTitle,
completedLessonCount: count
))
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
isNavigatingToCompletion = false
}
}
```
### 2.4 删除 onAppear 中「从完成页返回切回最后一节」逻辑
**删除**
```swift
if currentNodeId == "wg://completion", let lastId = allCourseNodes.last?.id {
currentNodeId = lastId
}
```
(去掉占位页后不再存在 `"wg://completion"` 选中的情况。)
### 2.5 删除 CompletionPlaceholderPage新增 LastPageSwipeModifier
**删除** `CompletionPlaceholderPage` 结构体。
**在** `// MARK: - 📄 单页课程视图` **之前** 新增:
```swift
// MARK: - 最后一页左滑:仅在最后一节挂载,左滑 push 完成页
private struct LastPageSwipeModifier: ViewModifier {
let isLastPage: Bool
let onSwipeLeft: () -> Void
func body(content: Content) -> some View {
content
.simultaneousGesture(
DragGesture(minimumDistance: 20, coordinateSpace: .local)
.onEnded { value in
guard isLastPage else { return }
if value.translation.width < -60,
abs(value.translation.width) > abs(value.translation.height) {
onSwipeLeft()
}
}
)
}
}
```
---
## 3. 调用方GrowthView / ProfileView / DiscoveryView
**无需改动**。CompletionView 仍为三参:`courseId`, `courseTitle`, `completedLessonCount`,依赖 `@EnvironmentObject navStore`,底部用 `switchToGrowthTab()` 回到技能页 Tab。
---
## 4. 小结
- **CompletionView**:无顶部返回、无打字机;赛博印章交互;右滑依赖 `SwipeBackEnablerModifier`;底部「回到我的内容」= `navStore.switchToGrowthTab()`
- **VerticalScreenPlayerView**:去掉占位页与 `onChange` 完成页逻辑;最后一节左滑用 `LastPageSwipeModifier` + `triggerCompletionNavigation` push 完成页;保留可选 `navigationPath`,笔记流不变。
- **不修改** GrowthView / ProfileView / DiscoveryView 的 `CompletionView(...)` 调用。

View File

@ -1,84 +0,0 @@
# 完成页迭代方案 — 审查报告(禁止应用)
**审查对象**:左滑进完成页、右滑回最后一节;无顶部返回;无打字机金句;底部「回到我的内容」回到技能页 Tab。
**结论**:只做审查与完整代码输出,不修改仓库文件。
---
## 一、需求与方案对照
| 需求 | 方案 | 审查结论 |
|------|------|----------|
| 不想要点开页面导航栏的返回按钮 | 移除顶部导航栏和返回按钮,仅顶部留白 | ✅ 一致 |
| 右滑回到最后一个小节 | `.enableSwipeBack()` + `SwipeBackEnabler` 强制开启侧滑返回 | ⚠️ 见下文「侧滑返回」 |
| 最后小节左滑进完成页,右滑回最后小节 | 播放器最后一节挂 `LastPageSwipeModifier` 左滑 push完成页右滑 = 系统 pop | ✅ 一致 |
| 不要打字机金句 | 移除打字机区域 | ✅ 一致 |
| 底部「回到我的内容」回到技能页 Tab | 方案写的是 `navigationPath = NavigationPath()` | ❌ **逻辑错误**,见下 |
---
## 二、关键问题:底部按钮语义
**需求**:底部「回到我的内容」要回到**技能页 Tab**(即技能 Tab 根:课程列表)。
**方案中的实现**`handleReturnToRoot()` 里写的是 `navigationPath = NavigationPath()`,即清空**当前传入的 path**。
- 若从**技能 Tab**进入:传的是 `growthPath`,清空后 = 技能 Tab 根 ✅
- 若从**发现 Tab**进入:传的是 `homePath`,清空后 = **发现 Tab 根**,不会切到技能 Tab ❌
- 若从**我的 Tab**进入:传的是 `profilePath`,清空后 = **我的 Tab 根**,不会切到技能 Tab ❌
因此:**仅清空当前 path 无法满足「无论从哪个 Tab 进,都回到技能页 Tab」**。
**正确做法**:底部按钮应调用 **`navStore.switchToGrowthTab()`**(切到技能 Tab + 清空 `growthPath`),与当前线上 CompletionView 的「继续学习」一致。
- CompletionView 需保留 **`@EnvironmentObject var navStore`**
- 不需要为「回到我的内容」传入 **`@Binding var navigationPath`**(调用方无需改传参)
**完整代码中已按此修正**:底部按钮调用 `navStore.switchToGrowthTab()`,不传 `navigationPath`
---
## 三、侧滑返回SwipeBackEnabler
- 方案用 **`SwipeBackEnabler`**`UIViewControllerRepresentable`)在 `.background` 里查找 `navigationController` 并打开 `interactivePopGestureRecognizer`,以在隐藏导航栏时恢复右滑返回。
- **风险**`makeUIViewController` 返回的是一颗裸 `UIViewController()`,其 `navigationController` 在部分时机可能仍为 nil例如尚未挂到 NavigationStack 上),`DispatchQueue.main.async` 能缓解但无法完全保证。若遇真机偶发无效,可考虑:
- 在 `updateUIViewController` 中轮询/延迟再取一次 `navigationController`,或
- 使用 `UINavigationController` 子类 / 注入方式保证拿到的必为当前栈。
- **结论**:实现可保留,建议在真机多场景(从三个 Tab 进入完成页后右滑)验证;若失效再加强时机或注入方式。
---
## 四、VerticalScreenPlayerView 与占位页
- 方案采用「**不使用占位页**,仅最后一节左滑手势 push 完成页」TabView 只渲染 `ForEach(allCourseNodes)`,不再多一页 `CompletionPlaceholderPage`
- **影响**
- 进入完成页**仅剩**「在最后一节左滑」这一种方式;「滑到下一空白页再进完成页」的路径被移除。
- 从完成页右滑或 dismiss 后,播放器不再存在「当前是占位页、需在 onAppear 里切回最后一节」的状态,可删除 `onAppear` 里对 `currentNodeId == "wg://completion"` 的处理。
- **与现有调用方**NoteTreeView / NoteListView 不传 `navigationPath``triggerCompletionNavigation` 里需 `guard let path = navigationPath else { return }`,从笔记进的播放器仍不会 push 完成页,行为不变。
---
## 五、CompletionView 视觉与接口
- **视觉**:从当前 3D 翻转卡片 + 打字机,改为「赛博印章」:圆环 + 未盖章「完」/ 盖章后「已完成的第 N 节」+ 底部「回到我的内容」。
- **接口**
- 保留:`courseId`, `courseTitle`, `completedLessonCount`
- 不增加 `@Binding var navigationPath`(底部用 `navStore.switchToGrowthTab()`)。
- 保留 `@EnvironmentObject var navStore`
- **调用方**GrowthView / ProfileView / DiscoveryView 的 `.completion` 分支**无需**新增参数,仍为三参构造。
---
## 六、审查结论汇总
| 项 | 结论 |
|----|------|
| 顶部去掉返回按钮 | ✅ 方案正确 |
| 右滑回最后一节 | ✅ 思路正确SwipeBackEnabler 需真机验证,偶发需加强时机 |
| 最后小节左滑进完成页 | ✅ 保留 LastPageSwipeModifier 即可 |
| 去掉打字机金句 | ✅ 方案正确 |
| 底部回到技能页 Tab | ❌ 方案用「清空当前 path」会错应用 `navStore.switchToGrowthTab()`,完整代码已改 |
| 调用方改动 | ✅ CompletionView 不需新参数VerticalScreenPlayerView 保持可选 `navigationPath`,笔记流不受影响 |
---
**完整代码见下(仅作交付,不写入仓库)。**

View File

@ -1,43 +0,0 @@
# 主题色 & 思考流 — 重复修改审查
## 1. 主题色 (theme_color)
| 位置 | 作用 | 是否重复 |
|------|------|----------|
| **backend/src/controllers/courseController.ts** | 管理后台创建课程后,`create` 不含 themeColor随后 `update` 写入 `generateThemeColor(course.id)` | 同一流程内只写一次,无重复 |
| **backend/src/services/taskService.ts** | 用户/AI 创建课程后,同样 create 再 update 写入 theme_color | 与 courseController 是**不同入口**(后台 vs 用户),不是同一逻辑改两遍 |
**结论:主题色没有“改重复”。** 两处对应两种创建路径,各自只设置一次。
---
## 2. 思考流 (thinking flow)
### 2.1 文案重复(同一份长文案存在两处)
| 位置 | 内容 |
|------|------|
| **backend/src/controllers/aiCourseController.ts** | `getThinkingFlow` 里返回的长文案(多段) |
| **ios/.../AICourseModels.swift** | `AICourseConstants.defaultThinkingFlowText`(多段) |
两处文案内容一致。当前前端只用本地常量,不再请求接口,但**后端仍保留同一份文案**。以后若改文案需要改两处,属于重复维护。
### 2.2 前端未使用的代码(相对“改重复”的冗余)
| 位置 | 说明 |
|------|------|
| **AICourseService.getThinkingFlow()** | 仍调用 `/api/ai/thinking-flow` 并带 5 分钟缓存,但 **CreationHubView 已不调用**,直接用 `AICourseConstants.defaultThinkingFlowText` |
| **CreateCourseInputView** | 已标记废弃,`thinkingFlowText` 初始为 `""`,未发现调用 `getThinkingFlow` |
即:思考流接口和缓存逻辑还在,但当前创建流程已不用,相当于死代码。
---
## 3. 总结
- **主题色**没有在同一流程里重复设置courseController 与 taskService 是不同入口,逻辑未改重复。
- **思考流**
- 有**文案重复**(后端 + iOS 各一份相同长文案)。
- 有**前端冗余**`getThinkingFlow` 与缓存未被 CreationHubView 使用。
若需要,我可以**只在前端**做收敛(例如删除或标注不再使用 getThinkingFlow 的调用、在注释中说明文案以后只维护 AICourseConstants 一处),**不改后端**。

View File

@ -1,86 +0,0 @@
# CardFrontView 错位印刷版 + CompletionView 微调 — 审查报告(禁止应用)
**审查对象**:① CardFrontView 重构为「错位印刷风格」Chromatic Aberration + 极粗宋体 + 紧凑叠字 + 胶囊标签);② CompletionView 主视图中漫射光晕透明度与卡片尺寸微调。
**结论**:仅审查,**不应用、不修改**仓库内任何文件。
---
## 一、变更范围概览
| 变更项 | 当前实现 | 交付代码 | 说明 |
|--------|----------|----------|------|
| **CardFrontView** | 竖排大字 180pt boldspacing -40offset ±20底部「点击开启」纯文字 + 呼吸动画 | 错位叠印(墨底 + 主层 blendMode、220pt blackspacing -65offset ±35/±10底部白色胶囊「点击开启」固定 opacity 0.8 | 视觉风格从「艺术字」升级为「新中式赛博印刷风」 |
| **CompletionView 光晕** | 两枚 Circle 透明度 0.06 / 0.05 | 0.04 / 0.04 | 卡片正面白底更显眼 |
| **CompletionView 卡片容器** | .frame(width: 280, height: 400)cornerRadius 24 | .frame(width: 300, height: 440)cornerRadius 24 | 竖版藏书票感、视觉冲击更强 |
---
## 二、CardFrontView 交付代码审查
### 2.1 设计逻辑与实现对应
| 设计点 | 实现方式 | 结论 |
|--------|----------|------|
| **错位叠印 (Chromatic Aberration)** | 同一 TypographyBody 画两遍:① 深紫灰墨底 offset(4,4) + blur(2);② 渐变主层 `.blendMode(.sourceAtop)` | ✅ 套色偏差感明确,有「印在纸上」的厚度 |
| **极粗宋体 (weight .black)** | 「完」「成」`.font(.system(size: 220, weight: .black, design: .serif))` | ✅ 笔画张力强,与「破纸而出」描述一致 |
| **紧凑排版 (spacing: -65)** | `VStack(spacing: -65)`,二字重叠 | ✅ 图形化图腾感,弱化可读、强化象征 |
| **标签胶囊** | 底部 HStack 包在 `Capsule().fill(Color.white).shadow(...)`padding bottom 32整体 opacity 0.8 | ✅ 不抢主视觉,又比纯文字更精致 |
### 2.2 与当前 CardFrontView 的差异
- **当前**:单层渐变字 + `drawingGroup()`底部为「arrow.up + 点击开启」纯文字 + `hintOpacity` 呼吸动画。
- **交付**:双层(墨底 + 渐变主层)、无 `drawingGroup()`,底部为胶囊包裹的「点击开启」、无动画。
- **命名**:交付代码仍为 `struct CardFrontView`,与当前一致,替换后 CompletionView 内 `CardFrontView(gradient: etherealGradient)` 无需改动。
### 2.3 需注意的点
| 项目 | 说明 |
|------|------|
| **shimmerOffset** | 交付代码中 `@State private var shimmerOffset: CGFloat = -0.5` 未在 body 内使用,属冗余状态,应用时可删除以免误导。 |
| **GeometryReader 与裁切** | 文字层用 `GeometryReader` + `.position(center)` 居中,外层 `.clipShape(RoundedRectangle(cornerRadius: 24))` 与当前一致,裁切行为正常。 |
| **噪点层** | `Color.gray.opacity(0.03)` 作为极淡纸张噪点,对性能影响可忽略。 |
---
## 三、CompletionView 主视图微调审查
交付说明中的两处修改与当前代码的对应关系如下。
### 3.1 漫射光晕透明度
- **当前**
`Color(red: 1.0, green: 0.45, blue: 0.85).opacity(0.06)`
`Color(red: 0.58, green: 0.28, blue: 0.95).opacity(0.05)`
- **交付**
两处均改为 `.opacity(0.04)`
效果:背景光晕更弱,卡片正面白底更纯粹,与「让卡片正面的白更显眼」一致。
### 3.2 卡片容器尺寸与圆角
- **当前**
`.frame(width: 280, height: 400)``.cornerRadius(24)` 已在父级或同层使用。
- **交付**
`.frame(width: 300, height: 440)`,圆角保持 24。
应用时需在 **CompletionView** 的 body 中,将包裹 `CardBackView` / `CardFrontView` 的那层 ZStack 的 `.frame(width: 280, height: 400)` 改为 `.frame(width: 300, height: 440)`;若该层未写 cornerRadius保持现有 `.cornerRadius(24)` 即可。
---
## 四、接口与调用方影响
- **CardFrontView**:仍为 `CardFrontView(gradient: LinearGradient)`仅内部实现与视觉变化CompletionView 调用处无需改。
- **CompletionView**:仅 body 内光晕透明度与卡片容器尺寸变化入参、导航、按钮逻辑均不变VerticalScreenPlayerView 等调用方无需改。
---
## 五、审查结论汇总
| 项目 | 结论 |
|------|------|
| **CardFrontView 错位印刷版** | 设计逻辑清晰,墨底 + sourceAtop、220pt black、spacing -65、胶囊标签均与「新中式赛博印刷风」描述一致`shimmerOffset` 未使用可删。 |
| **CompletionView 光晕** | 0.06/0.05 → 0.04/0.04 合理,正面白更突出。 |
| **CompletionView 卡片尺寸** | 280×400 → 300×440 与「竖版藏书票」描述一致。 |
| **接口与其它页面** | 无新增参数、无改动导航或 Tab仅完结页内部视觉与尺寸调整。 |
**未对仓库内任何文件进行修改。**

View File

@ -0,0 +1,12 @@
// Develop.xcconfig - 开发/测试环境
// 用于连接测试服、预发布环境
#include "Shared.xcconfig"
// API 域名(注入 Info.plist运行时通过 Bundle.main 读取)
API_DOMAIN = https://api.muststudy.xin
INFOPLIST_KEY_API_DOMAIN = $(API_DOMAIN)
// Swift 编译条件:代码中可用 #if API_ENV_DEVELOP
SWIFT_ACTIVE_COMPILATION_CONDITIONS = API_ENV_DEVELOP $(inherited)
GCC_PREPROCESSOR_DEFINITIONS = $(inherited)

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_DOMAIN</key>
<string>$(API_DOMAIN)</string>
</dict>
</plist>

View File

@ -0,0 +1,13 @@
// Local.xcconfig - 本地开发环境
// 用于连接本机 localhost 或局域网后端
// 真机调试时可将 API_DOMAIN 改为本机局域网 IP如 http://192.168.1.100:3000
#include "Shared.xcconfig"
// API 域名(注入 Info.plist运行时通过 Bundle.main 读取)
API_DOMAIN = http://localhost:3000
INFOPLIST_KEY_API_DOMAIN = $(API_DOMAIN)
// Swift 编译条件:代码中可用 #if API_ENV_LOCAL
SWIFT_ACTIVE_COMPILATION_CONDITIONS = API_ENV_LOCAL $(inherited)
GCC_PREPROCESSOR_DEFINITIONS = $(inherited)

View File

@ -0,0 +1,12 @@
// Online.xcconfig - 线上生产环境
// 用于 Release 打包、TestFlight、App Store
#include "Shared.xcconfig"
// API 域名(注入 Info.plist运行时通过 Bundle.main 读取)
API_DOMAIN = https://wildgrowth.upolar.com
INFOPLIST_KEY_API_DOMAIN = $(API_DOMAIN)
// Swift 编译条件:代码中可用 #if API_ENV_ONLINE
SWIFT_ACTIVE_COMPILATION_CONDITIONS = API_ENV_ONLINE $(inherited)
GCC_PREPROCESSOR_DEFINITIONS = $(inherited)

View File

@ -0,0 +1,6 @@
// Shared.xcconfig - 公共配置(被各环境 xcconfig 引用)
// 包含 Swift 版本、编译选项等通用设置
SWIFT_VERSION = 5.0
SWIFT_EMIT_LOC_STRINGS = YES
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES

View File

@ -1,259 +0,0 @@
# SF Symbol 使用清单
项目中所有使用 SF Symbol 的位置及对应 symbol 名称。
---
## 一、按文件列出
### VerticalScreenPlayerView.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 56 | `arrow.left` | 返回按钮 |
| 154 | `exclamationmark.triangle` | 错误/警告提示 |
| 209 | `book.closed` | 课程/书本占位 |
| 814 | `exclamationmark.triangle` | 错误态占位 |
### MapView.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 84 | `exclamationmark.triangle` | 错误提示 |
| 344 | `arrow.left` | 返回按钮 |
| 409 | `iconName`(动态) | 地图头水印,来自 `data.watermarkIcon` |
| 415 | `book.closed.fill` | 地图头水印兜底(无 watermarkIcon 时) |
| 597 | `lock.fill` | 节点锁定状态 |
| 607 | `checkmark` | 节点已完成 |
| 618 | `play.fill` | 节点可播放 |
### LoginView.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 51 | `xmark` | 关闭弹窗 |
| 106 | `checkmark.square.fill` / `square` | 同意协议勾选(已选/未选) |
| 176 | `applelogo` | 苹果登录按钮 |
| 364 | `arrow.left` | 返回按钮 |
### ProfileView.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 285 | `lock.circle.fill` | 锁定课程标识 |
| 393 | `course.watermarkIconName`(动态) | 课程卡片水印 |
| 421 | `course.watermarkIconName`(动态) | 课程卡片水印 |
| 530 | `exclamationmark.triangle.fill` | 错误提示 |
| 629 | `chevron.right` | 右箭头/进入 |
| 738 | `book.closed` | 空状态/占位 |
| 814 | `xmark.circle.fill` | 关闭/删除 |
| 713 | `pencil` | Label「编辑信息」 |
| 720 | `trash` | Label「删除」 |
### Views/Profile/CyberLearningIDCard.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 174 | `square.and.pencil` | 编辑 |
| 196 | `person.fill` | 头像占位 |
### NoteBottomSheetView.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 52 | `quote.opening` | 引用/摘录 |
| 95 | `trash` | 删除 |
| 211 | `ellipsis` | 更多菜单 |
### Views/GrowthView.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 71 | `book.closed` | 课程占位 |
| 208 | `lock.circle.fill` | 锁定课程 |
| 578 | `exclamationmark.circle.fill` / `checkmark.circle.fill` | Toast 类型图标 |
| 628 | `trash` | Label「移除课程」 |
| 635 | `doc.text` | Label「查看详情」 |
| 647 | `pencil` | Label「修改课程名称」 |
| 656 | `trash` | Label「移除课程」 |
### Views/Growth/GrowthTopBar.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 22 | `plus` | 添加按钮 |
### ArticleRichTextView.swiftUIKit 菜单)
| 行号 | Symbol | 用途 |
|------|--------|------|
| 740 | `highlighter` | 划线 |
| 745 | `bubble.left.and.bubble.right` | 写想法 |
| 750 | `doc.on.doc` | 复制 |
### Views/DiscoveryComponents.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 63 | `iconName`(动态) | 发现流卡片水印,来自 `item.watermarkIcon` |
| 145 | `iconName`(动态) | 信息流卡片水印,来自 `course.watermarkIcon` |
### Views/CreationHubView.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 86 | `arrow.up.circle.fill` | 上传/发布 |
| 194 | `checkmark.circle.fill` | 成功状态 |
| 219 | `xmark.circle.fill` | 失败/关闭 |
| 460 | `icon`(动态) | 列表行图标,见下方取值 |
| 473 | `chevron.right` | 右箭头 |
| 557 | `book.closed` | 空状态 |
**CreationHubRow 的 `icon` 取值(写死在调用处):**
- `folder.fill` — 我的创作
- `doc.text.fill` — 从文档创建
- `arrow.triangle.2.circlepath` — 从已有课程复制
### DesignSystem.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 337 | `photo` | 占位图/无图 |
### Views/PersonaSelectionView.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 102 | `chevron.left` | 返回 |
| 172 | `checkmark.circle.fill` | 已选 |
| 177 | `circle` | 未选 |
### Views/CreateCourseInputView.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 158 | `doc.fill` | 文档 |
| 180 | `doc.fill` | 文档 |
| 199 | `xmark.circle.fill` | 关闭/清除 |
### Views/GenerationProgressView.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 84 | `exclamationmark.triangle.fill` | 错误 |
| 163 | `checkmark.circle.fill` | 成功 |
| 198 | `xmark` | 关闭窗口 |
### NotebookListView.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 117 | `lock.doc.fill` | 锁定笔记本 |
| 221 | `book.closed` | 空状态 |
| 193 | `pencil` | Label「编辑信息」 |
| 200 | `trash` | Label「删除」 |
### Views/Profile/ProfileNoteListView.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 16 | `note.text` | 笔记列表 |
| 113 | `book.closed` | 空状态 |
### Views/Profile/EditProfileSheet.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 52 | `person.fill` | 头像占位 |
| 65 | `person.fill` | 头像占位 |
| 77 | `camera.fill` | 拍照/相册 |
### Views/Profile/AvatarPicker.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 14 | `photo.stack` | 相册选择 |
### PaywallView.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 31 | `xmark.circle.fill` | 关闭 |
| 45 | `lock.open.fill` | 解锁/付费入口 |
| 133 | `checkmark.circle.fill` | 权益项勾选 |
### ToastView.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 23 | `exclamationmark.circle.fill` / `checkmark.circle.fill` / `info.circle.fill` | 按 Toast 类型error/success/info |
### NoteTreeView.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 136 | `square.and.pencil` | 编辑 |
### NoteListView.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 73 | `note.text` | 笔记列表 |
| 218 | `book.closed.fill` | 空状态 |
| 257 | `highlighter` | 划线 |
| 307 | `note.text.badge.plus` | 新建笔记 |
### NoteInputView.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 35 | `quote.opening` | 引用样式 |
### Views/NoteTree/NoteTreeRow.swift
| 行号 | Symbol | 用途 |
|------|--------|------|
| 72 | `pencil` | Label「编辑」 |
| 76 | `trash` | Label「删除」 |
---
## 二、动态 Symbol 来源汇总
| 使用处 | 变量/属性 | 可能取值(来源) |
|--------|-----------|------------------|
| MapView 地图头水印 | `data.watermarkIcon` | 后端 API无则兜底 `book.closed.fill` |
| DiscoveryComponents 卡片水印 | `item.watermarkIcon` / `course.watermarkIcon` | 后端 API |
| ProfileView 课程水印 | `course.watermarkIconName` | 见 CourseModels 逻辑 |
**CourseModels.watermarkIconName 逻辑:**
- 若有 `watermarkIcon` 且非空 → 用该值
- 否则按 `type``system` → `book.closed.fill``single` → `doc.text.fill``vertical_screen` → `rectangle.portrait.fill`,默认 → `doc.text.fill`
---
## 三、按 Symbol 名称汇总(便于替换/统一)
| Symbol | 出现位置(简要) |
|--------|------------------|
| `arrow.left` | VerticalScreenPlayerView, MapView, LoginView 返回 |
| `arrow.up.circle.fill` | CreationHubView 上传 |
| `arrow.triangle.2.circlepath` | CreationHubView 从课程复制 |
| `applelogo` | LoginView 苹果登录 |
| `book.closed` | VerticalScreenPlayerView, ProfileView, GrowthView, CreationHubView, NotebookListView, ProfileNoteListView 占位/空态 |
| `book.closed.fill` | MapView 水印兜底CourseModels 中 type=systemNoteListView 空态 |
| `bubble.left.and.bubble.right` | ArticleRichTextView 写想法 |
| `camera.fill` | EditProfileSheet |
| `checkmark` | MapView 节点完成 |
| `checkmark.circle.fill` | 多处成功/已选/权益勾选 |
| `checkmark.square.fill` / `square` | LoginView 协议勾选 |
| `chevron.left` | PersonaSelectionView 返回 |
| `chevron.right` | ProfileView, CreationHubView 进入下一级 |
| `circle` | PersonaSelectionView 未选 |
| `doc.fill` | CreateCourseInputView |
| `doc.on.doc` | ArticleRichTextView 复制 |
| `doc.text` | GrowthView Label 查看详情 |
| `doc.text.fill` | CreationHubView 从文档创建CourseModels single/默认 |
| `ellipsis` | NoteBottomSheetView 更多 |
| `exclamationmark.circle.fill` | GrowthView/ToastView 错误 |
| `exclamationmark.triangle` | VerticalScreenPlayerView, MapView 警告 |
| `exclamationmark.triangle.fill` | ProfileView, GenerationProgressView 错误 |
| `folder.fill` | CreationHubView 我的创作 |
| `highlighter` | ArticleRichTextView 划线NoteListView 划线 |
| `info.circle.fill` | ToastView info 类型 |
| `lock.fill` | MapView 节点锁定 |
| `lock.circle.fill` | ProfileView, GrowthView 课程锁定 |
| `lock.doc.fill` | NotebookListView 笔记本锁定 |
| `lock.open.fill` | PaywallView 解锁 |
| `note.text` | ProfileNoteListView, NoteListView |
| `note.text.badge.plus` | NoteListView 新建笔记 |
| `pencil` | ProfileView, GrowthView, NotebookListView, NoteTreeRow Label 编辑 |
| `person.fill` | CyberLearningIDCard, EditProfileSheet |
| `photo` | DesignSystem 占位 |
| `photo.stack` | AvatarPicker |
| `play.fill` | MapView 节点播放 |
| `quote.opening` | NoteBottomSheetView, NoteInputView 引用 |
| `rectangle.portrait.fill` | CourseModels vertical_screen 水印 |
| `square.and.pencil` | CyberLearningIDCard, NoteTreeView 编辑 |
| `trash` | 多处 Label/按钮 删除 |
| `xmark` | LoginView, GenerationProgressView 关闭 |
| `xmark.circle.fill` | ProfileView, CreationHubView, CreateCourseInputView, PaywallView 关闭/清除 |
---
*文档根据当前代码静态分析生成,若新增或修改 SF Symbol 请同步更新此清单。*

View File

@ -3,37 +3,13 @@ import Foundation
class APIClient { class APIClient {
static let shared = APIClient() static let shared = APIClient()
#if DEBUG // MARK: - ( xcconfig API_DOMAIN Info.plist)
// MARK: - (Debug) // Xcode Build Configuration
var baseURL: String { var baseURL: String {
// (Bundle.main.infoDictionary?["API_DOMAIN"] as? String) ?? "https://api.muststudy.xin"
// true = Debug 线 (线 HTTPS )
// false = Debug localhost ()
let useProduction = false // <--- 便
if useProduction {
return "https://api.muststudy.xin"
}
#if targetEnvironment(simulator)
// 访
return "http://localhost:3000"
#else
// ()
// false IP
return "http://192.168.1.100:3000"
#endif
} }
#else
// MARK: - (Release/TestFlight)
/// 使线 / TestFlight
var baseURL: String { "https://api.muststudy.xin" }
#endif
// URLSession session // URLSession session
private let session: URLSession private let session: URLSession

View File

@ -7,6 +7,14 @@ struct WildGrowthApp: App {
@StateObject private var userManager = UserManager.shared @StateObject private var userManager = UserManager.shared
init() { init() {
// 🌐 API 便 xcconfig
let rawAPI = Bundle.main.infoDictionary?["API_DOMAIN"] as? String
let baseURL = APIClient.shared.baseURL
print("🌐 [API] Info.plist API_DOMAIN = \(rawAPI ?? "nil")")
print("🌐 [API] APIClient.baseURL = \(baseURL)")
if rawAPI == nil {
print("⚠️ [API] API_DOMAIN 未从 xcconfig 注入,使用默认值。请检查 Build Configuration 和 INFOPLIST_KEY_API_DOMAIN")
}
// 🔥 Kingfisher // 🔥 Kingfisher
configureKingfisher() configureKingfisher()
// V1.0 + // V1.0 +

View File

@ -1,94 +0,0 @@
# 字号优化说明 - 2026年2月
## 📊 优化原则
**核心理念**:调整略小的字号至合理层级,保持已经合理的字号不变,确保 UI 一致性和良好的阅读体验。
---
## ✅ 已调整的字号(略小 → 合理)
### 1. 播放器正文区域 (`ContentBlockBuilder.swift`)
| 元素 | 原字号 | 新字号 | 调整理由 |
|------|--------|--------|---------|
| 正文 | 17pt | **19pt** | 提升长文阅读舒适度,符合阅读类应用标准 |
| 主标题 | 28pt | **30pt** | 与正文字号成比例协调 |
| H1 标题 | 24pt | **26pt** | 与正文字号成比例协调 |
| H2 标题 | 20pt | **22pt** | 与正文字号成比例协调 |
| 高亮文本 | 18pt | **20pt** | 与正文字号成比例协调 |
| 行间距 | 9pt | **10pt** | 保持黄金比例 1.72x (19×1.72≈32.7) |
| 段落间距 | 22pt | **24pt** | 随正文字号增大 |
**影响范围**
- ✅ `VerticalScreenPlayerView` - 播放器正文渲染
- ✅ `ArticleRichTextViewRepresentable` - 富文本显示
---
### 2. 地图页 (`MapView.swift`)
| 元素 | 原字号 | 新字号 | 调整理由 |
|------|--------|--------|---------|
| 章节标题 | 20pt | **22pt** | 增强区块分割感,提升层级清晰度 |
| 课程标题 | 16pt | **17pt** | 略微偏小,提升至标准阅读字号 |
| 进度信息 | caption (11-12pt) | **label (13pt)** | caption 过小,调整至 label 防止难以阅读 |
**影响范围**
- ✅ `VerticalListLayout` - 章节标题
- ✅ `LessonListRow` - 课程条目标题
- ✅ `AtmosphericHeaderView` - 悬浮卡片进度信息
---
## 🔒 保持不变的合理字号
以下字号已处于最佳层级,**无需调整**
| 区域 | 元素 | 字号 | 评价 |
|------|------|------|------|
| **MapView** | 头部大标题 | 32pt | ✅ 适当,具有冲击力 |
| **ProfileView** | 统计数字 | 32pt | ✅ 适当,数据展示清晰 |
| **DiscoveryView** | 运营位标题 | 22pt | ✅ 适当,信息层级合理 |
| **GrowthTopBar** | 导航标签 | 18pt (active) / 16pt (inactive) | ✅ 适当,选中态明显 |
| **DesignSystem** | 各类按钮与标签 | 13pt-16pt | ✅ 适当,符合 iOS 设计规范 |
| **ProfileView** | 笔记入口卡片 | 16pt (title) / 13pt (label) | ✅ 适当,层级清晰 |
| **MapView** | 悬浮卡片按钮 | 15pt | ✅ 适当,易于点击 |
---
## 📈 字号层级体系(完整)
| 层级 | 字号范围 | 用途 | 示例 |
|------|----------|------|------|
| **Display** | 30-32pt | 页面主标题、数据展示 | 播放器主标题 (30pt)、统计数字 (32pt) |
| **H1** | 26-28pt | 一级标题 | 播放器 H1 (26pt) |
| **H2** | 22-24pt | 二级标题、模块标题 | 地图章节标题 (22pt)、发现页标题 (22pt) |
| **H3** | 18-20pt | 三级标题、Tab 标签 | 播放器高亮 (20pt)、Tab Bar 选中 (18pt) |
| **Body** | 17-19pt | 正文、列表项 | 播放器正文 (19pt)、地图课程标题 (17pt) |
| **Label** | 13-16pt | 辅助信息、按钮 | 进度信息 (13pt)、按钮文字 (16pt) |
| **Caption** | 11-12pt | 极小辅助文本(谨慎使用) | ⚠️ 已基本避免使用 |
---
## 🎯 设计原则总结
1. **阅读优先**正文区域字号要足够大19pt确保长时间阅读不疲劳
2. **比例协调**:标题与正文成比例缩放,保持视觉节奏
3. **层级清晰**:字号差异明显(至少 2-4pt确保信息层级清晰
4. **避免过小**:尽量避免使用 caption11-12pt最小使用 label13pt
5. **符合规范**:参考 Apple HIG 和主流阅读应用(微信读书、即刻等)
---
## 📝 后续建议
1. **播放器体验**:可考虑根据用户反馈,提供字号调节功能(小、中、大)
2. **无障碍支持**确保所有文字支持动态字体Dynamic Type
3. **对比测试**:可通过 A/B 测试验证字号调整对阅读完成率的影响
4. **一致性检查**:定期审查新增页面,确保字号符合设计系统
---
**优化完成日期**2026年2月1日
**Git 提交记录**`2a44791` - 优化字号层级,提升阅读体验

View File

@ -7,8 +7,7 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
A328ABC72F01FCC20031E45F /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = A328ABC62F01FCC20031E45F /* Kingfisher */; }; 907A366B2F3DC0A200A4BA77 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = A328ABC62F01FCC20031E45F /* Kingfisher */; };
A38376772F1489CF0027969A /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = A38376762F1489CF0027969A /* MarkdownUI */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -32,6 +31,9 @@
A357EF6C2EE29B130004B865 /* WildGrowth.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WildGrowth.app; sourceTree = BUILT_PRODUCTS_DIR; }; A357EF6C2EE29B130004B865 /* WildGrowth.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WildGrowth.app; sourceTree = BUILT_PRODUCTS_DIR; };
A357EF7D2EE29B140004B865 /* WildGrowthTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WildGrowthTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; A357EF7D2EE29B140004B865 /* WildGrowthTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WildGrowthTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
A357EF872EE29B140004B865 /* WildGrowthUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WildGrowthUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; A357EF872EE29B140004B865 /* WildGrowthUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WildGrowthUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
A3CF00022EE29B130004B865 /* Online.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Online.xcconfig; sourceTree = "<group>"; };
A3CF00032EE29B130004B865 /* Develop.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Develop.xcconfig; sourceTree = "<group>"; };
A3CF00042EE29B130004B865 /* Local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Local.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFileSystemSynchronizedRootGroup section */
@ -57,8 +59,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
A328ABC72F01FCC20031E45F /* Kingfisher in Frameworks */, 907A366B2F3DC0A200A4BA77 /* Kingfisher in Frameworks */,
A38376772F1489CF0027969A /* MarkdownUI in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -92,6 +93,7 @@
A357EF6E2EE29B130004B865 /* WildGrowth */, A357EF6E2EE29B130004B865 /* WildGrowth */,
A357EF802EE29B140004B865 /* WildGrowthTests */, A357EF802EE29B140004B865 /* WildGrowthTests */,
A357EF8A2EE29B140004B865 /* WildGrowthUITests */, A357EF8A2EE29B140004B865 /* WildGrowthUITests */,
A3CF00012EE29B130004B865 /* Config */,
A328ABC52F01FCC20031E45F /* Frameworks */, A328ABC52F01FCC20031E45F /* Frameworks */,
A357EF6D2EE29B130004B865 /* Products */, A357EF6D2EE29B130004B865 /* Products */,
); );
@ -107,6 +109,16 @@
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
A3CF00012EE29B130004B865 /* Config */ = {
isa = PBXGroup;
children = (
A3CF00022EE29B130004B865 /* Online.xcconfig */,
A3CF00032EE29B130004B865 /* Develop.xcconfig */,
A3CF00042EE29B130004B865 /* Local.xcconfig */,
);
path = Config;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@ -129,7 +141,6 @@
name = WildGrowth; name = WildGrowth;
packageProductDependencies = ( packageProductDependencies = (
A328ABC62F01FCC20031E45F /* Kingfisher */, A328ABC62F01FCC20031E45F /* Kingfisher */,
A38376762F1489CF0027969A /* MarkdownUI */,
); );
productName = WildGrowth; productName = WildGrowth;
productReference = A357EF6C2EE29B130004B865 /* WildGrowth.app */; productReference = A357EF6C2EE29B130004B865 /* WildGrowth.app */;
@ -188,6 +199,9 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
BuildIndependentTargetsInParallel = 1; BuildIndependentTargetsInParallel = 1;
KnownAssetTags = (
New,
);
LastSwiftUpdateCheck = 1620; LastSwiftUpdateCheck = 1620;
LastUpgradeCheck = 1620; LastUpgradeCheck = 1620;
TargetAttributes = { TargetAttributes = {
@ -214,8 +228,8 @@
mainGroup = A357EF632EE29B130004B865; mainGroup = A357EF632EE29B130004B865;
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
packageReferences = ( packageReferences = (
A328ABC22F01FA250031E45F /* XCLocalSwiftPackageReference "../../../../Downloads/Kingfisher-master" */, 907A352B2F3CA3B000A4BA77 /* XCRemoteSwiftPackageReference "Kingfisher" */,
A38376752F1489CF0027969A /* XCRemoteSwiftPackageReference "MarkdownUI" */, 907A352C2F3CA60C00A4BA77 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */,
); );
preferredProjectObjectVersion = 77; preferredProjectObjectVersion = 77;
productRefGroup = A357EF6D2EE29B130004B865 /* Products */; productRefGroup = A357EF6D2EE29B130004B865 /* Products */;
@ -295,8 +309,202 @@
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
A357EF8F2EE29B140004B865 /* Debug */ = { A357EF922EE29B140004B865 /* Debug-Online */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00022EE29B130004B865 /* Online.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES;
CODE_SIGN_ENTITLEMENTS = WildGrowth/WildGrowth.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"WildGrowth/Preview Content\"";
DEVELOPMENT_TEAM = JMT5SK3SWY;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Config/Info-API.plist";
INFOPLIST_KEY_CFBundleDisplayName = "电子成长";
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.books";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.5;
MARKETING_VERSION = 1.1.0;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.mustmaster.WildGrowth;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
XROS_DEPLOYMENT_TARGET = 2.2;
};
name = "Debug-Online";
};
A357EF932EE29B140004B865 /* Release-Online */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00022EE29B130004B865 /* Online.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES;
CODE_SIGN_ENTITLEMENTS = WildGrowth/WildGrowth.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"WildGrowth/Preview Content\"";
DEVELOPMENT_TEAM = JMT5SK3SWY;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = "电子成长";
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.books";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.5;
MARKETING_VERSION = 1.1.0;
ONLY_ACTIVE_ARCH = NO;
PRODUCT_BUNDLE_IDENTIFIER = com.mustmaster.WildGrowth;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
XROS_DEPLOYMENT_TARGET = 2.2;
};
name = "Release-Online";
};
A357EF952EE29B140004B865 /* Debug-Online */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00022EE29B130004B865 /* Online.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = TGTAAHD84B;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MACOSX_DEPLOYMENT_TARGET = 14.5;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mustmaster.WildGrowthTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WildGrowth.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/WildGrowth";
XROS_DEPLOYMENT_TARGET = 2.2;
};
name = "Debug-Online";
};
A357EF962EE29B140004B865 /* Release-Online */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00022EE29B130004B865 /* Online.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = TGTAAHD84B;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MACOSX_DEPLOYMENT_TARGET = 14.5;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mustmaster.WildGrowthTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WildGrowth.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/WildGrowth";
XROS_DEPLOYMENT_TARGET = 2.2;
};
name = "Release-Online";
};
A357EF982EE29B140004B865 /* Debug-Online */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00022EE29B130004B865 /* Online.xcconfig */;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = TGTAAHD84B;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MACOSX_DEPLOYMENT_TARGET = 14.5;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mustmaster.WildGrowthUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TEST_TARGET_NAME = WildGrowth;
XROS_DEPLOYMENT_TARGET = 2.2;
};
name = "Debug-Online";
};
A357EF992EE29B140004B865 /* Release-Online */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00022EE29B130004B865 /* Online.xcconfig */;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = TGTAAHD84B;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MACOSX_DEPLOYMENT_TARGET = 14.5;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mustmaster.WildGrowthUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TEST_TARGET_NAME = WildGrowth;
XROS_DEPLOYMENT_TARGET = 2.2;
};
name = "Release-Online";
};
A3CF00112EE29B130004B865 /* Debug-Online */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00022EE29B130004B865 /* Online.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
@ -354,10 +562,11 @@
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
}; };
name = Debug; name = "Debug-Online";
}; };
A357EF902EE29B140004B865 /* Release */ = { A3CF00122EE29B130004B865 /* Release-Online */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00022EE29B130004B865 /* Online.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
@ -407,10 +616,243 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
}; };
name = Release; name = "Release-Online";
}; };
A357EF922EE29B140004B865 /* Debug */ = { A3CF00132EE29B130004B865 /* Debug-Develop */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00032EE29B130004B865 /* Develop.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = "Debug-Develop";
};
A3CF00142EE29B130004B865 /* Release-Develop */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00032EE29B130004B865 /* Develop.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SWIFT_COMPILATION_MODE = wholemodule;
};
name = "Release-Develop";
};
A3CF00152EE29B130004B865 /* Debug-Local */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00042EE29B130004B865 /* Local.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = "Debug-Local";
};
A3CF00162EE29B130004B865 /* Release-Local */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00042EE29B130004B865 /* Local.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SWIFT_COMPILATION_MODE = wholemodule;
};
name = "Release-Local";
};
A3CF00212EE29B130004B865 /* Debug-Develop */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00032EE29B130004B865 /* Develop.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
@ -421,10 +863,11 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"WildGrowth/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"WildGrowth/Preview Content\"";
DEVELOPMENT_TEAM = TGTAAHD84B; DEVELOPMENT_TEAM = JMT5SK3SWY;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Config/Info-API.plist";
INFOPLIST_KEY_CFBundleDisplayName = "电子成长"; INFOPLIST_KEY_CFBundleDisplayName = "电子成长";
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.books"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.books";
@ -455,10 +898,11 @@
TARGETED_DEVICE_FAMILY = 1; TARGETED_DEVICE_FAMILY = 1;
XROS_DEPLOYMENT_TARGET = 2.2; XROS_DEPLOYMENT_TARGET = 2.2;
}; };
name = Debug; name = "Debug-Develop";
}; };
A357EF932EE29B140004B865 /* Release */ = { A3CF00222EE29B130004B865 /* Release-Develop */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00032EE29B130004B865 /* Develop.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
@ -469,10 +913,11 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"WildGrowth/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"WildGrowth/Preview Content\"";
DEVELOPMENT_TEAM = TGTAAHD84B; DEVELOPMENT_TEAM = JMT5SK3SWY;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Config/Info-API.plist";
INFOPLIST_KEY_CFBundleDisplayName = "电子成长"; INFOPLIST_KEY_CFBundleDisplayName = "电子成长";
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.books"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.books";
@ -503,10 +948,113 @@
TARGETED_DEVICE_FAMILY = 1; TARGETED_DEVICE_FAMILY = 1;
XROS_DEPLOYMENT_TARGET = 2.2; XROS_DEPLOYMENT_TARGET = 2.2;
}; };
name = Release; name = "Release-Develop";
}; };
A357EF952EE29B140004B865 /* Debug */ = { A3CF00232EE29B130004B865 /* Debug-Local */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00042EE29B130004B865 /* Local.xcconfig */;
buildSettings = {
API_DOMAIN = "";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES;
CODE_SIGN_ENTITLEMENTS = WildGrowth/WildGrowth.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"WildGrowth/Preview Content\"";
DEVELOPMENT_TEAM = JMT5SK3SWY;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Config/Info-API.plist";
INFOPLIST_KEY_CFBundleDisplayName = "电子成长";
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.books";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.5;
MARKETING_VERSION = 1.1.0;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.mustmaster.WildGrowth;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
XROS_DEPLOYMENT_TARGET = 2.2;
};
name = "Debug-Local";
};
A3CF00242EE29B130004B865 /* Release-Local */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00042EE29B130004B865 /* Local.xcconfig */;
buildSettings = {
API_DOMAIN = "";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES;
CODE_SIGN_ENTITLEMENTS = WildGrowth/WildGrowth.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"WildGrowth/Preview Content\"";
DEVELOPMENT_TEAM = JMT5SK3SWY;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Config/Info-API.plist";
INFOPLIST_KEY_CFBundleDisplayName = "电子成长";
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.books";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.5;
MARKETING_VERSION = 1.1.0;
ONLY_ACTIVE_ARCH = NO;
PRODUCT_BUNDLE_IDENTIFIER = com.mustmaster.WildGrowth;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
XROS_DEPLOYMENT_TARGET = 2.2;
};
name = "Release-Local";
};
A3CF00252EE29B130004B865 /* Debug-Develop */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00032EE29B130004B865 /* Develop.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@ -526,10 +1074,11 @@
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WildGrowth.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/WildGrowth"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WildGrowth.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/WildGrowth";
XROS_DEPLOYMENT_TARGET = 2.2; XROS_DEPLOYMENT_TARGET = 2.2;
}; };
name = Debug; name = "Debug-Develop";
}; };
A357EF962EE29B140004B865 /* Release */ = { A3CF00262EE29B130004B865 /* Release-Develop */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00032EE29B130004B865 /* Develop.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@ -549,10 +1098,59 @@
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WildGrowth.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/WildGrowth"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WildGrowth.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/WildGrowth";
XROS_DEPLOYMENT_TARGET = 2.2; XROS_DEPLOYMENT_TARGET = 2.2;
}; };
name = Release; name = "Release-Develop";
}; };
A357EF982EE29B140004B865 /* Debug */ = { A3CF00272EE29B130004B865 /* Debug-Local */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00042EE29B130004B865 /* Local.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = TGTAAHD84B;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MACOSX_DEPLOYMENT_TARGET = 14.5;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mustmaster.WildGrowthTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WildGrowth.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/WildGrowth";
XROS_DEPLOYMENT_TARGET = 2.2;
};
name = "Debug-Local";
};
A3CF00282EE29B130004B865 /* Release-Local */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00042EE29B130004B865 /* Local.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = TGTAAHD84B;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MACOSX_DEPLOYMENT_TARGET = 14.5;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mustmaster.WildGrowthTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WildGrowth.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/WildGrowth";
XROS_DEPLOYMENT_TARGET = 2.2;
};
name = "Release-Local";
};
A3CF00292EE29B130004B865 /* Debug-Develop */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00032EE29B130004B865 /* Develop.xcconfig */;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
@ -571,10 +1169,11 @@
TEST_TARGET_NAME = WildGrowth; TEST_TARGET_NAME = WildGrowth;
XROS_DEPLOYMENT_TARGET = 2.2; XROS_DEPLOYMENT_TARGET = 2.2;
}; };
name = Debug; name = "Debug-Develop";
}; };
A357EF992EE29B140004B865 /* Release */ = { A3CF002A2EE29B130004B865 /* Release-Develop */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00032EE29B130004B865 /* Develop.xcconfig */;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
@ -593,7 +1192,53 @@
TEST_TARGET_NAME = WildGrowth; TEST_TARGET_NAME = WildGrowth;
XROS_DEPLOYMENT_TARGET = 2.2; XROS_DEPLOYMENT_TARGET = 2.2;
}; };
name = Release; name = "Release-Develop";
};
A3CF002B2EE29B130004B865 /* Debug-Local */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00042EE29B130004B865 /* Local.xcconfig */;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = TGTAAHD84B;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MACOSX_DEPLOYMENT_TARGET = 14.5;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mustmaster.WildGrowthUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TEST_TARGET_NAME = WildGrowth;
XROS_DEPLOYMENT_TARGET = 2.2;
};
name = "Debug-Local";
};
A3CF002C2EE29B130004B865 /* Release-Local */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00042EE29B130004B865 /* Local.xcconfig */;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = TGTAAHD84B;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MACOSX_DEPLOYMENT_TARGET = 14.5;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mustmaster.WildGrowthUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TEST_TARGET_NAME = WildGrowth;
XROS_DEPLOYMENT_TARGET = 2.2;
};
name = "Release-Local";
}; };
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
@ -601,52 +1246,69 @@
A357EF672EE29B130004B865 /* Build configuration list for PBXProject "电子成长" */ = { A357EF672EE29B130004B865 /* Build configuration list for PBXProject "电子成长" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
A357EF8F2EE29B140004B865 /* Debug */, A3CF00112EE29B130004B865 /* Debug-Online */,
A357EF902EE29B140004B865 /* Release */, A3CF00122EE29B130004B865 /* Release-Online */,
A3CF00132EE29B130004B865 /* Debug-Develop */,
A3CF00142EE29B130004B865 /* Release-Develop */,
A3CF00152EE29B130004B865 /* Debug-Local */,
A3CF00162EE29B130004B865 /* Release-Local */,
); );
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = "Release-Online";
}; };
A357EF912EE29B140004B865 /* Build configuration list for PBXNativeTarget "WildGrowth" */ = { A357EF912EE29B140004B865 /* Build configuration list for PBXNativeTarget "WildGrowth" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
A357EF922EE29B140004B865 /* Debug */, A357EF922EE29B140004B865 /* Debug-Online */,
A357EF932EE29B140004B865 /* Release */, A357EF932EE29B140004B865 /* Release-Online */,
A3CF00212EE29B130004B865 /* Debug-Develop */,
A3CF00222EE29B130004B865 /* Release-Develop */,
A3CF00232EE29B130004B865 /* Debug-Local */,
A3CF00242EE29B130004B865 /* Release-Local */,
); );
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = "Release-Online";
}; };
A357EF942EE29B140004B865 /* Build configuration list for PBXNativeTarget "WildGrowthTests" */ = { A357EF942EE29B140004B865 /* Build configuration list for PBXNativeTarget "WildGrowthTests" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
A357EF952EE29B140004B865 /* Debug */, A357EF952EE29B140004B865 /* Debug-Online */,
A357EF962EE29B140004B865 /* Release */, A357EF962EE29B140004B865 /* Release-Online */,
A3CF00252EE29B130004B865 /* Debug-Develop */,
A3CF00262EE29B130004B865 /* Release-Develop */,
A3CF00272EE29B130004B865 /* Debug-Local */,
A3CF00282EE29B130004B865 /* Release-Local */,
); );
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = "Release-Online";
}; };
A357EF972EE29B140004B865 /* Build configuration list for PBXNativeTarget "WildGrowthUITests" */ = { A357EF972EE29B140004B865 /* Build configuration list for PBXNativeTarget "WildGrowthUITests" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
A357EF982EE29B140004B865 /* Debug */, A357EF982EE29B140004B865 /* Debug-Online */,
A357EF992EE29B140004B865 /* Release */, A357EF992EE29B140004B865 /* Release-Online */,
A3CF00292EE29B130004B865 /* Debug-Develop */,
A3CF002A2EE29B130004B865 /* Release-Develop */,
A3CF002B2EE29B130004B865 /* Debug-Local */,
A3CF002C2EE29B130004B865 /* Release-Local */,
); );
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = "Release-Online";
}; };
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
A328ABC22F01FA250031E45F /* XCLocalSwiftPackageReference "../../../../Downloads/Kingfisher-master" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = "../../../../Downloads/Kingfisher-master";
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
A38376752F1489CF0027969A /* XCRemoteSwiftPackageReference "MarkdownUI" */ = { 907A352B2F3CA3B000A4BA77 /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/gonzalezreal/MarkdownUI"; repositoryURL = "https://github.com/onevcat/Kingfisher";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 8.6.2;
};
};
907A352C2F3CA60C00A4BA77 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/gonzalezreal/swift-markdown-ui";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = upToNextMajorVersion;
minimumVersion = 2.4.1; minimumVersion = 2.4.1;
@ -657,19 +1319,14 @@
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
A328ABC32F01FA6A0031E45F /* Kingfisher */ = { A328ABC32F01FA6A0031E45F /* Kingfisher */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = A328ABC22F01FA250031E45F /* XCLocalSwiftPackageReference "../../../../Downloads/Kingfisher-master" */; package = 907A352B2F3CA3B000A4BA77 /* XCRemoteSwiftPackageReference "Kingfisher" */;
productName = Kingfisher; productName = Kingfisher;
}; };
A328ABC62F01FCC20031E45F /* Kingfisher */ = { A328ABC62F01FCC20031E45F /* Kingfisher */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = A328ABC22F01FA250031E45F /* XCLocalSwiftPackageReference "../../../../Downloads/Kingfisher-master" */; package = 907A352B2F3CA3B000A4BA77 /* XCRemoteSwiftPackageReference "Kingfisher" */;
productName = Kingfisher; productName = Kingfisher;
}; };
A38376762F1489CF0027969A /* MarkdownUI */ = {
isa = XCSwiftPackageProductDependency;
package = A38376752F1489CF0027969A /* XCRemoteSwiftPackageReference "MarkdownUI" */;
productName = MarkdownUI;
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
}; };
rootObject = A357EF642EE29B130004B865 /* Project object */; rootObject = A357EF642EE29B130004B865 /* Project object */;

View File

@ -1,13 +1,13 @@
{ {
"originHash" : "f430e49d6b841dc65fa45490167bf3212b889ddf3e49a795fe473a7f0c2af4ac", "originHash" : "18350c2bfa3935125b6f4e9817e7ed4508588c07142d420b8b8ee00640a57853",
"pins" : [ "pins" : [
{ {
"identity" : "markdownui", "identity" : "kingfisher",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/MarkdownUI", "location" : "https://github.com/onevcat/Kingfisher",
"state" : { "state" : {
"revision" : "5f613358148239d0292c0cef674a3c2314737f9e", "revision" : "d30a5fad881137e2267f96a8e3fc35c58999bb94",
"version" : "2.4.1" "version" : "8.6.2"
} }
}, },
{ {
@ -27,6 +27,15 @@
"revision" : "5d9bdaa4228b381639fff09403e39a04926e2dbe", "revision" : "5d9bdaa4228b381639fff09403e39a04926e2dbe",
"version" : "0.7.1" "version" : "0.7.1"
} }
},
{
"identity" : "swift-markdown-ui",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/swift-markdown-ui",
"state" : {
"revision" : "5f613358148239d0292c0cef674a3c2314737f9e",
"version" : "2.4.1"
}
} }
], ],
"version" : 3 "version" : 3

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildLocationStyle</key>
<string>UseAppPreferences</string>
<key>CustomBuildLocationType</key>
<string>RelativeToDerivedData</string>
<key>DerivedDataLocationStyle</key>
<string>Default</string>
<key>ShowSharedSchemesAutomaticallyEnabled</key>
<true/>
</dict>
</plist>

View File

@ -24,7 +24,7 @@
</BuildActionEntries> </BuildActionEntries>
</BuildAction> </BuildAction>
<TestAction <TestAction
buildConfiguration = "Debug" buildConfiguration = "Debug-Local"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES"
@ -55,7 +55,7 @@
</Testables> </Testables>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Release-Online"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0" launchStyle = "0"
@ -76,7 +76,7 @@
</BuildableProductRunnable> </BuildableProductRunnable>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release-Online"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = "" savedToolIdentifier = ""
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
@ -93,10 +93,10 @@
</BuildableProductRunnable> </BuildableProductRunnable>
</ProfileAction> </ProfileAction>
<AnalyzeAction <AnalyzeAction
buildConfiguration = "Debug"> buildConfiguration = "Debug-Local">
</AnalyzeAction> </AnalyzeAction>
<ArchiveAction <ArchiveAction
buildConfiguration = "Release" buildConfiguration = "Release-Online"
revealArchiveInOrganizer = "YES"> revealArchiveInOrganizer = "YES">
</ArchiveAction> </ArchiveAction>
</Scheme> </Scheme>