线上配置
This commit is contained in:
parent
498599e67a
commit
48dbd4b49f
|
|
@ -27,7 +27,7 @@ jobs:
|
|||
rm -rf repo
|
||||
git clone "${REPO_URL}" repo
|
||||
|
||||
- name: Copy backend to /data/wildgrowth/weizhuozhongzhi-ai
|
||||
- name: Copy backend to /data/wildgrowth/backend
|
||||
run: |
|
||||
set -e
|
||||
cd repo
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no "$SERVER" << EOF
|
|||
echo "✅ 已更新 APPLE_SHARED_SECRET"
|
||||
else
|
||||
echo "APPLE_SHARED_SECRET=$SECRET" >> .env
|
||||
echo "✅ 已添加 APPLE_SHARED_SECRET"
|
||||
echo "✅ 已添加 "
|
||||
fi
|
||||
|
||||
# 验证配置
|
||||
|
|
|
|||
|
|
@ -1,3 +1,21 @@
|
|||
#后端启动端口号配置
|
||||
PORT=3005
|
||||
|
||||
#数据库配置
|
||||
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
|
||||
|
|
@ -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 延时可视产品需求保留或去掉。 |
|
||||
|
||||
**未对仓库内任何文件进行修改。**
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
# CompletionView Magic Card 版 — 审查报告(不应用)
|
||||
|
||||
**审查日期**:2025-01-29
|
||||
**范围**:Magic Card Edition(粒子庆祝 + 按钮卡片一体化 + 极光光晕)
|
||||
**结论**:仅审查、不修改仓库;逻辑继承正确,**ParticleModifier 存在两处需修复的实现问题**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 需求落实情况
|
||||
|
||||
| 项目 | 要求 | 定稿实现 | 结论 |
|
||||
|------|------|----------|------|
|
||||
| **按钮卡片一体化** | 按钮在卡片内,点击后「献祭」化作彩带和数据 | IdleActionView 中按钮,切换后显示 ActiveDashboard + ConfettiExplosion | ✅ |
|
||||
| **粒子爆炸** | 蓝/紫/粉色粒子炸开 | ConfettiExplosion:Circle + Capsule,confettiColors | ✅ |
|
||||
| **呼吸光晕** | 激活后卡片背景极淡极光渐变 | 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 的角度单位与随机值稳定性,否则粒子动画会出现方向错误和抖动。**本次未对仓库做任何修改。**
|
||||
|
|
@ -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。
|
||||
- **符合度**:方案与当前都符合;方案在视图层也不再依赖「多一页」的 tag,SSOT 更纯粹。
|
||||
|
||||
### 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` 传参做清单检查,避免漏传/错传。 |
|
||||
|
||||
---
|
||||
|
||||
**报告日期**:基于当前代码与所提供方案整理,未对仓库做任何代码修改。
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
# CompletionView Skeleton & Reveal 版 — 审查报告(不应用)
|
||||
|
||||
**审查日期**:2025-01-29
|
||||
**范围**:Skeleton & Reveal Edition(纯白卡片 + 呼吸骨架屏 + 蓝色弥散光 + 粒子庆祝)
|
||||
**结论**:仅审查、不修改仓库;需求落实正确,ParticleModifier 已按前次审查完成修复,**有一处可选视觉层级调整**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 需求落实情况
|
||||
|
||||
| 项目 | 要求 | 定稿实现 | 结论 |
|
||||
|------|------|----------|------|
|
||||
| **纯白卡片** | 彻底去「脏」,回归纯白 | `background(Color.white)` | ✅ |
|
||||
| **拒绝空状态** | 未激活时用骨架屏替代留白 | IdleSkeletonView:RoundedRectangle 骨架 + 呼吸透明度 | ✅ |
|
||||
| **呼吸骨架** | 暗示「数据等你揭开」 | `isBreathing ? 0.3 : 0.6` 配合 `repeatForever(autoreverses: true)` | ✅ |
|
||||
| **蓝色弥散光** | 激活后四周泛蓝光 | `shadow(color: isSystemOn ? brandBlue.opacity(0.25) : ..., radius: 40)` | ✅ |
|
||||
| **彩带爆炸** | 激活瞬间粒子庆祝 | ConfettiExplosion(Circle + 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。**本次未对仓库做任何修改。**
|
||||
|
|
@ -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 回滚”与“游客短路”两项建议,逻辑与现版对齐,可作为最终发布候选。**本次未对仓库做任何修改。**
|
||||
|
|
@ -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。**本次未对仓库做任何修改。**
|
||||
|
|
@ -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.swift(f725f31 内)
|
||||
|
||||
| 变更 | 说明 |
|
||||
|------|------|
|
||||
| 占位页 | `CompletionPlaceholderPage` 由「带 courseId/courseTitle/navigationPath + onAppear 里 push」改为「纯视觉占位」 |
|
||||
| 新增 | TabView 上 `.onChange(of: currentNodeId)`,当 `newId == "wg://completion"` 时 0.1s 后 append `.completion` |
|
||||
|
||||
**影响**:完成页进入方式从「占位页 onAppear」改为「父视图 onChange」,避免预加载误触。
|
||||
|
||||
---
|
||||
|
||||
## 三、当前工作区未提交改动(仅 1 个文件)
|
||||
|
||||
| 文件 | 变更概要 |
|
||||
|------|----------|
|
||||
| **VerticalScreenPlayerView.swift** | 在 f725f31 基础上:CompletionPlaceholderPage 去掉参数、改为纯 ZStack;TabView 增加 `.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** 还有未提交修改,其他页面没有新的未提交变更。
|
||||
|
||||
---
|
||||
|
||||
**未对仓库进行任何应用或修改操作。**
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
# CompletionView iOS 原生风格版 — 定稿审查报告(不应用)
|
||||
|
||||
**审查日期**:2025-01-29
|
||||
**范围**:Final iOS Native Style(systemGroupedBackground + 白卡片 + 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`,无编译冲突。**本次未对仓库做任何修改。**
|
||||
|
|
@ -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 传参一致。
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
# 完成页「强力侧滑版」— 审查报告(禁止应用)
|
||||
|
||||
**审查对象**:粉紫勋章视觉 + RobustSwipeBackEnabler(didMove + 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 等不动。 |
|
||||
|
||||
**未对仓库内任何文件进行修改。**
|
||||
|
|
@ -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`;若后续决定采用,可直接用提案代码替换整个文件,并视需要做上述图标小改。
|
||||
|
|
@ -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,无 sort;allItems = realNodes.map(.lesson) + append(.completion)。与当前「章节顺序 + 数组顺序」一致。✅
|
||||
- **错误态 / toast / hideTabBar / showTabBar / handleBack(path 优先)**:均保留。✅
|
||||
- **LessonPageView**:传 `self.courseTitle ?? mapData?.courseTitle`、navigationPath;headerConfig 仍用 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/传参)不变。✅ |
|
||||
| **其他页面 / 功能** | 接口与调用方式未改,其他页面、其他功能不受影响。✅ |
|
||||
|
||||
**未对仓库内任何文件进行修改。**
|
||||
|
|
@ -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,保留同文件其余代码;逻辑与展示符合「逻辑回滚 + 统一分页」目标 ✅ |
|
||||
| **其他页面** | 无需改动,零影响 ✅ |
|
||||
|
||||
**未对仓库内任何文件进行修改。**
|
||||
|
|
@ -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,再在该章内取 validNodes(filter + 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,无 sort;allItems = realNodes.map(.lesson) + append(.completion)。
|
||||
- **isFirstNodeInChapter**:含 `.sorted { $0.order < $1.order }`,与当前仓库等价。
|
||||
- **getChapterTitle**:与当前一致。
|
||||
- **currentPositionProgress**:仅按 lesson 项计算,忽略完结页。
|
||||
- **handleBack / 错误态 / toast / hideTabBar / showTabBar**:均保留。
|
||||
- **LessonPageView**:传 `self.courseTitle ?? mapData?.courseTitle`、navigationPath;headerConfig 仍用 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,保留同文件其余代码;其他逻辑与展示不变。 |
|
||||
| **现有逻辑与展示** | 在按上述范围替换的前提下,不会受到影响。 |
|
||||
|
||||
**未对仓库内任何文件进行修改。**
|
||||
|
|
@ -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 | 未改枚举与三 Tab;CompletionView 三参 | ✅ |
|
||||
| 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 数据源、loadMapData(realNodes + 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 语义决定。 |
|
||||
|
||||
**未对仓库内任何文件进行修改。**
|
||||
|
|
@ -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 → 数字 → LESSONS;下:FOCUS 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 线性滚动 |
|
||||
| **接口兼容** | ✅ 可直接替换 |
|
||||
|
||||
**审查结论**:定稿代码满足设计要求,逻辑继承正确,可直接用于替换。**本次未对仓库做任何修改。**
|
||||
|
|
@ -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 等业务数据。**
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
# 完成页「统一分页架构」— 零影响审查报告(禁止应用)
|
||||
|
||||
**审查目标**:若应用此方案,确保**其他页面、其他功能、其他逻辑一点也不受影响**。
|
||||
**结论**:仅审查,不修改仓库内任何文件。下文列出所有受影响点及达成「零影响」的**必要条件**。
|
||||
|
||||
---
|
||||
|
||||
## 一、方案会动到的文件与接口
|
||||
|
||||
| 文件 | 方案中的改动 |
|
||||
|------|----------------|
|
||||
| **CompletionView.swift** | 整文件替换:去掉 courseId;仅保留 courseTitle、completedLessonCount;纯 UI + switchToGrowthTab |
|
||||
| **VerticalScreenPlayerView.swift** | 数据源改为 allItems (PlayerItem);去掉占位页与 onChange;init 去掉 navigationPath、isLastNode、courseTitle;handleBack 仅 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 / showTabBar(onAppear / 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)**保持一致**。
|
||||
|
||||
**未对仓库内任何文件进行修改。**
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
# 完成页「统一分页 + 零影响」代码审查报告(禁止应用)
|
||||
|
||||
**审查对象**:严格遵守 6 条零影响条件的 CompletionView + VerticalScreenPlayerView 完整代码(内部重构,外部兼容)。
|
||||
**结论**:仅审查,不修改仓库内任何文件。对照 6 条逐项核对,并列出需补全/修正的细节以达「绝对零影响」。
|
||||
|
||||
---
|
||||
|
||||
## 一、6 条零影响条件对照
|
||||
|
||||
| # | 条件 | 本版代码 | 结论 |
|
||||
|---|------|----------|------|
|
||||
| 1 | VerticalScreenPlayerView init 保持 6 参 | `init(courseId, nodeId, initialScrollIndex, navigationPath?, isLastNode?, courseTitle?)` 完整保留 | ✅ 满足 |
|
||||
| 2 | 保留 CourseNavigation.completion 及三处 destination | 未改枚举与三 Tab;CompletionView 三参,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) + completion,id 分别为 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 三参未变 |
|
||||
| MapView(append .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。 |
|
||||
| **其他页面 / 功能 / 逻辑** | 在以上补全前提下,其他页面、其他功能、其他逻辑均不受影响。 |
|
||||
|
||||
**未对仓库内任何文件进行修改。**
|
||||
|
|
@ -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 优点
|
||||
|
||||
- **交互统一**:左滑/右滑完全由 TabView(UIScrollView)负责,无需自定义 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 做展示。 |
|
||||
| 统一分页架构本身 | 交互简单、无需手势与侧滑 Hack,CompletionView 可做纯 UI;若应用需处理 init 与调用方兼容、以及错误/toast/tabBar 等现有能力是否保留。 |
|
||||
|
||||
**未对仓库内任何文件进行修改。**
|
||||
|
|
@ -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,若暂无专注时长)
|
||||
|
||||
**其他页面**:当前无其它地方使用 CompletionView,CourseNavigation 也无 `.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`。 |
|
||||
|
||||
**未对仓库内任何文件进行修改。**
|
||||
|
|
@ -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()`;② 移除或对接「上传学习数据」与「专注时长」逻辑,与现有统计与导航一致。 |
|
||||
|
||||
**未对仓库内任何文件进行修改。**
|
||||
|
|
@ -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
|
||||
|
||||
**原逻辑**(约 146–186 行):
|
||||
`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(...)` 调用。
|
||||
|
|
@ -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`,笔记流不受影响 |
|
||||
|
||||
---
|
||||
|
||||
**完整代码见下(仅作交付,不写入仓库)。**
|
||||
|
|
@ -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 一处),**不改后端**。
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
# CardFrontView 错位印刷版 + CompletionView 微调 — 审查报告(禁止应用)
|
||||
|
||||
**审查对象**:① CardFrontView 重构为「错位印刷风格」(Chromatic Aberration + 极粗宋体 + 紧凑叠字 + 胶囊标签);② CompletionView 主视图中漫射光晕透明度与卡片尺寸微调。
|
||||
**结论**:仅审查,**不应用、不修改**仓库内任何文件。
|
||||
|
||||
---
|
||||
|
||||
## 一、变更范围概览
|
||||
|
||||
| 变更项 | 当前实现 | 交付代码 | 说明 |
|
||||
|--------|----------|----------|------|
|
||||
| **CardFrontView** | 竖排大字 180pt bold,spacing -40,offset ±20,底部「点击开启」纯文字 + 呼吸动画 | 错位叠印(墨底 + 主层 blendMode)、220pt black,spacing -65,offset ±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,仅完结页内部视觉与尺寸调整。 |
|
||||
|
||||
**未对仓库内任何文件进行修改。**
|
||||
|
|
@ -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.swift(UIKit 菜单)
|
||||
| 行号 | 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=system;NoteListView 空态 |
|
||||
| `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 请同步更新此清单。*
|
||||
|
|
@ -3,37 +3,13 @@ import Foundation
|
|||
class APIClient {
|
||||
static let shared = APIClient()
|
||||
|
||||
#if DEBUG
|
||||
// MARK: - 开发环境配置 (Debug)
|
||||
|
||||
// MARK: - 环境配置 (由 xcconfig 的 API_DOMAIN 注入 Info.plist)
|
||||
// 在 Xcode 中切换 Build Configuration 即可切换环境
|
||||
|
||||
var baseURL: String {
|
||||
// 【⚠️ 生产环境调试开关】
|
||||
// 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
|
||||
(Bundle.main.infoDictionary?["API_DOMAIN"] as? String) ?? "https://api.muststudy.xin"
|
||||
}
|
||||
|
||||
#else
|
||||
// MARK: - 生产环境配置 (Release/TestFlight)
|
||||
|
||||
/// 生产环境固定使用线上域名,打包 / TestFlight 必走此处
|
||||
var baseURL: String { "https://api.muststudy.xin" }
|
||||
|
||||
#endif
|
||||
|
||||
// ✅ 复用 URLSession,避免每次请求都创建新的 session
|
||||
private let session: URLSession
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,14 @@ struct WildGrowthApp: App {
|
|||
@StateObject private var userManager = UserManager.shared
|
||||
|
||||
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 配置,提升图片加载速度
|
||||
configureKingfisher()
|
||||
// ✅ V1.0 埋点:初始化 + 记录冷启动
|
||||
|
|
|
|||
|
|
@ -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. **避免过小**:尽量避免使用 caption(11-12pt),最小使用 label(13pt)
|
||||
5. **符合规范**:参考 Apple HIG 和主流阅读应用(微信读书、即刻等)
|
||||
|
||||
---
|
||||
|
||||
## 📝 后续建议
|
||||
|
||||
1. **播放器体验**:可考虑根据用户反馈,提供字号调节功能(小、中、大)
|
||||
2. **无障碍支持**:确保所有文字支持动态字体(Dynamic Type)
|
||||
3. **对比测试**:可通过 A/B 测试验证字号调整对阅读完成率的影响
|
||||
4. **一致性检查**:定期审查新增页面,确保字号符合设计系统
|
||||
|
||||
---
|
||||
|
||||
**优化完成日期**:2026年2月1日
|
||||
**Git 提交记录**:`2a44791` - 优化字号层级,提升阅读体验
|
||||
|
|
@ -7,8 +7,7 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
A328ABC72F01FCC20031E45F /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = A328ABC62F01FCC20031E45F /* Kingfisher */; };
|
||||
A38376772F1489CF0027969A /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = A38376762F1489CF0027969A /* MarkdownUI */; };
|
||||
907A366B2F3DC0A200A4BA77 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = A328ABC62F01FCC20031E45F /* Kingfisher */; };
|
||||
/* End PBXBuildFile 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; };
|
||||
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; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
|
|
@ -57,8 +59,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A328ABC72F01FCC20031E45F /* Kingfisher in Frameworks */,
|
||||
A38376772F1489CF0027969A /* MarkdownUI in Frameworks */,
|
||||
907A366B2F3DC0A200A4BA77 /* Kingfisher in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -92,6 +93,7 @@
|
|||
A357EF6E2EE29B130004B865 /* WildGrowth */,
|
||||
A357EF802EE29B140004B865 /* WildGrowthTests */,
|
||||
A357EF8A2EE29B140004B865 /* WildGrowthUITests */,
|
||||
A3CF00012EE29B130004B865 /* Config */,
|
||||
A328ABC52F01FCC20031E45F /* Frameworks */,
|
||||
A357EF6D2EE29B130004B865 /* Products */,
|
||||
);
|
||||
|
|
@ -107,6 +109,16 @@
|
|||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A3CF00012EE29B130004B865 /* Config */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A3CF00022EE29B130004B865 /* Online.xcconfig */,
|
||||
A3CF00032EE29B130004B865 /* Develop.xcconfig */,
|
||||
A3CF00042EE29B130004B865 /* Local.xcconfig */,
|
||||
);
|
||||
path = Config;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
|
@ -129,7 +141,6 @@
|
|||
name = WildGrowth;
|
||||
packageProductDependencies = (
|
||||
A328ABC62F01FCC20031E45F /* Kingfisher */,
|
||||
A38376762F1489CF0027969A /* MarkdownUI */,
|
||||
);
|
||||
productName = WildGrowth;
|
||||
productReference = A357EF6C2EE29B130004B865 /* WildGrowth.app */;
|
||||
|
|
@ -188,6 +199,9 @@
|
|||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
KnownAssetTags = (
|
||||
New,
|
||||
);
|
||||
LastSwiftUpdateCheck = 1620;
|
||||
LastUpgradeCheck = 1620;
|
||||
TargetAttributes = {
|
||||
|
|
@ -214,8 +228,8 @@
|
|||
mainGroup = A357EF632EE29B130004B865;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
A328ABC22F01FA250031E45F /* XCLocalSwiftPackageReference "../../../../Downloads/Kingfisher-master" */,
|
||||
A38376752F1489CF0027969A /* XCRemoteSwiftPackageReference "MarkdownUI" */,
|
||||
907A352B2F3CA3B000A4BA77 /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
||||
907A352C2F3CA60C00A4BA77 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = A357EF6D2EE29B130004B865 /* Products */;
|
||||
|
|
@ -295,8 +309,202 @@
|
|||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
A357EF8F2EE29B140004B865 /* Debug */ = {
|
||||
A357EF922EE29B140004B865 /* Debug-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_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 = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
|
|
@ -354,10 +562,11 @@
|
|||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
name = "Debug-Online";
|
||||
};
|
||||
A357EF902EE29B140004B865 /* Release */ = {
|
||||
A3CF00122EE29B130004B865 /* Release-Online */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A3CF00022EE29B130004B865 /* Online.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
|
|
@ -407,10 +616,243 @@
|
|||
MTL_FAST_MATH = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
};
|
||||
name = Release;
|
||||
name = "Release-Online";
|
||||
};
|
||||
A357EF922EE29B140004B865 /* Debug */ = {
|
||||
A3CF00132EE29B130004B865 /* Debug-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;
|
||||
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 = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
|
|
@ -421,10 +863,11 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"WildGrowth/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = TGTAAHD84B;
|
||||
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";
|
||||
|
|
@ -455,10 +898,11 @@
|
|||
TARGETED_DEVICE_FAMILY = 1;
|
||||
XROS_DEPLOYMENT_TARGET = 2.2;
|
||||
};
|
||||
name = Debug;
|
||||
name = "Debug-Develop";
|
||||
};
|
||||
A357EF932EE29B140004B865 /* Release */ = {
|
||||
A3CF00222EE29B130004B865 /* Release-Develop */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A3CF00032EE29B130004B865 /* Develop.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
|
|
@ -469,10 +913,11 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"WildGrowth/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = TGTAAHD84B;
|
||||
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";
|
||||
|
|
@ -503,10 +948,113 @@
|
|||
TARGETED_DEVICE_FAMILY = 1;
|
||||
XROS_DEPLOYMENT_TARGET = 2.2;
|
||||
};
|
||||
name = Release;
|
||||
name = "Release-Develop";
|
||||
};
|
||||
A357EF952EE29B140004B865 /* Debug */ = {
|
||||
A3CF00232EE29B130004B865 /* Debug-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 = 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 = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
|
|
@ -526,10 +1074,11 @@
|
|||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WildGrowth.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/WildGrowth";
|
||||
XROS_DEPLOYMENT_TARGET = 2.2;
|
||||
};
|
||||
name = Debug;
|
||||
name = "Debug-Develop";
|
||||
};
|
||||
A357EF962EE29B140004B865 /* Release */ = {
|
||||
A3CF00262EE29B130004B865 /* Release-Develop */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A3CF00032EE29B130004B865 /* Develop.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
|
|
@ -549,10 +1098,59 @@
|
|||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WildGrowth.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/WildGrowth";
|
||||
XROS_DEPLOYMENT_TARGET = 2.2;
|
||||
};
|
||||
name = Release;
|
||||
name = "Release-Develop";
|
||||
};
|
||||
A357EF982EE29B140004B865 /* Debug */ = {
|
||||
A3CF00272EE29B130004B865 /* Debug-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 = "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 = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
|
|
@ -571,10 +1169,11 @@
|
|||
TEST_TARGET_NAME = WildGrowth;
|
||||
XROS_DEPLOYMENT_TARGET = 2.2;
|
||||
};
|
||||
name = Debug;
|
||||
name = "Debug-Develop";
|
||||
};
|
||||
A357EF992EE29B140004B865 /* Release */ = {
|
||||
A3CF002A2EE29B130004B865 /* Release-Develop */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A3CF00032EE29B130004B865 /* Develop.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
|
|
@ -593,7 +1192,53 @@
|
|||
TEST_TARGET_NAME = WildGrowth;
|
||||
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 */
|
||||
|
||||
|
|
@ -601,52 +1246,69 @@
|
|||
A357EF672EE29B130004B865 /* Build configuration list for PBXProject "电子成长" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
A357EF8F2EE29B140004B865 /* Debug */,
|
||||
A357EF902EE29B140004B865 /* Release */,
|
||||
A3CF00112EE29B130004B865 /* Debug-Online */,
|
||||
A3CF00122EE29B130004B865 /* Release-Online */,
|
||||
A3CF00132EE29B130004B865 /* Debug-Develop */,
|
||||
A3CF00142EE29B130004B865 /* Release-Develop */,
|
||||
A3CF00152EE29B130004B865 /* Debug-Local */,
|
||||
A3CF00162EE29B130004B865 /* Release-Local */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
defaultConfigurationName = "Release-Online";
|
||||
};
|
||||
A357EF912EE29B140004B865 /* Build configuration list for PBXNativeTarget "WildGrowth" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
A357EF922EE29B140004B865 /* Debug */,
|
||||
A357EF932EE29B140004B865 /* Release */,
|
||||
A357EF922EE29B140004B865 /* Debug-Online */,
|
||||
A357EF932EE29B140004B865 /* Release-Online */,
|
||||
A3CF00212EE29B130004B865 /* Debug-Develop */,
|
||||
A3CF00222EE29B130004B865 /* Release-Develop */,
|
||||
A3CF00232EE29B130004B865 /* Debug-Local */,
|
||||
A3CF00242EE29B130004B865 /* Release-Local */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
defaultConfigurationName = "Release-Online";
|
||||
};
|
||||
A357EF942EE29B140004B865 /* Build configuration list for PBXNativeTarget "WildGrowthTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
A357EF952EE29B140004B865 /* Debug */,
|
||||
A357EF962EE29B140004B865 /* Release */,
|
||||
A357EF952EE29B140004B865 /* Debug-Online */,
|
||||
A357EF962EE29B140004B865 /* Release-Online */,
|
||||
A3CF00252EE29B130004B865 /* Debug-Develop */,
|
||||
A3CF00262EE29B130004B865 /* Release-Develop */,
|
||||
A3CF00272EE29B130004B865 /* Debug-Local */,
|
||||
A3CF00282EE29B130004B865 /* Release-Local */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
defaultConfigurationName = "Release-Online";
|
||||
};
|
||||
A357EF972EE29B140004B865 /* Build configuration list for PBXNativeTarget "WildGrowthUITests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
A357EF982EE29B140004B865 /* Debug */,
|
||||
A357EF992EE29B140004B865 /* Release */,
|
||||
A357EF982EE29B140004B865 /* Debug-Online */,
|
||||
A357EF992EE29B140004B865 /* Release-Online */,
|
||||
A3CF00292EE29B130004B865 /* Debug-Develop */,
|
||||
A3CF002A2EE29B130004B865 /* Release-Develop */,
|
||||
A3CF002B2EE29B130004B865 /* Debug-Local */,
|
||||
A3CF002C2EE29B130004B865 /* Release-Local */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
defaultConfigurationName = "Release-Online";
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
A328ABC22F01FA250031E45F /* XCLocalSwiftPackageReference "../../../../Downloads/Kingfisher-master" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = "../../../../Downloads/Kingfisher-master";
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
A38376752F1489CF0027969A /* XCRemoteSwiftPackageReference "MarkdownUI" */ = {
|
||||
907A352B2F3CA3B000A4BA77 /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
|
||||
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 = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.4.1;
|
||||
|
|
@ -657,19 +1319,14 @@
|
|||
/* Begin XCSwiftPackageProductDependency section */
|
||||
A328ABC32F01FA6A0031E45F /* Kingfisher */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = A328ABC22F01FA250031E45F /* XCLocalSwiftPackageReference "../../../../Downloads/Kingfisher-master" */;
|
||||
package = 907A352B2F3CA3B000A4BA77 /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||
productName = Kingfisher;
|
||||
};
|
||||
A328ABC62F01FCC20031E45F /* Kingfisher */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = A328ABC22F01FA250031E45F /* XCLocalSwiftPackageReference "../../../../Downloads/Kingfisher-master" */;
|
||||
package = 907A352B2F3CA3B000A4BA77 /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||
productName = Kingfisher;
|
||||
};
|
||||
A38376762F1489CF0027969A /* MarkdownUI */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = A38376752F1489CF0027969A /* XCRemoteSwiftPackageReference "MarkdownUI" */;
|
||||
productName = MarkdownUI;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = A357EF642EE29B130004B865 /* Project object */;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"originHash" : "f430e49d6b841dc65fa45490167bf3212b889ddf3e49a795fe473a7f0c2af4ac",
|
||||
"originHash" : "18350c2bfa3935125b6f4e9817e7ed4508588c07142d420b8b8ee00640a57853",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "markdownui",
|
||||
"identity" : "kingfisher",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/gonzalezreal/MarkdownUI",
|
||||
"location" : "https://github.com/onevcat/Kingfisher",
|
||||
"state" : {
|
||||
"revision" : "5f613358148239d0292c0cef674a3c2314737f9e",
|
||||
"version" : "2.4.1"
|
||||
"revision" : "d30a5fad881137e2267f96a8e3fc35c58999bb94",
|
||||
"version" : "8.6.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -27,6 +27,15 @@
|
|||
"revision" : "5d9bdaa4228b381639fff09403e39a04926e2dbe",
|
||||
"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
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
buildConfiguration = "Debug-Local"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
buildConfiguration = "Release-Online"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
|
|
@ -76,7 +76,7 @@
|
|||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
buildConfiguration = "Release-Online"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
|
|
@ -93,10 +93,10 @@
|
|||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
buildConfiguration = "Debug-Local">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
buildConfiguration = "Release-Online"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
|
|||
Loading…
Reference in New Issue