@tencent-rtc/trtc-agent-skills 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/README.zh.md +173 -0
- package/bin/cli.js +434 -0
- package/knowledge-base/index.yaml +454 -0
- package/knowledge-base/platform-slice-template.md +233 -0
- package/knowledge-base/scenario-spec.md +350 -0
- package/knowledge-base/scenarios/conference/base/general-conference.md +365 -0
- package/knowledge-base/scenarios/conference/base/webinar-conference.md +130 -0
- package/knowledge-base/scenarios/conference/medical/1v1-video-consultation.md +145 -0
- package/knowledge-base/scenarios/conference/medical/medical-multidoctor-consultation.md +113 -0
- package/knowledge-base/scenarios/live/entertainment-live-room.md +118 -0
- package/knowledge-base/slice-spec.md +546 -0
- package/knowledge-base/slices/conference/web/ai-tools.md +225 -0
- package/knowledge-base/slices/conference/web/beauty-effects.md +188 -0
- package/knowledge-base/slices/conference/web/device-control.md +338 -0
- package/knowledge-base/slices/conference/web/login-auth.md +261 -0
- package/knowledge-base/slices/conference/web/network-quality.md +190 -0
- package/knowledge-base/slices/conference/web/official-roomkit-api.md +298 -0
- package/knowledge-base/slices/conference/web/official-roomkit-login-ui.md +246 -0
- package/knowledge-base/slices/conference/web/participant-list.md +238 -0
- package/knowledge-base/slices/conference/web/participant-management.md +718 -0
- package/knowledge-base/slices/conference/web/prejoin-check.md +293 -0
- package/knowledge-base/slices/conference/web/room-call.md +213 -0
- package/knowledge-base/slices/conference/web/room-chat.md +426 -0
- package/knowledge-base/slices/conference/web/room-lifecycle.md +534 -0
- package/knowledge-base/slices/conference/web/room-schedule.md +281 -0
- package/knowledge-base/slices/conference/web/screen-share.md +211 -0
- package/knowledge-base/slices/conference/web/video-layout.md +675 -0
- package/knowledge-base/slices/conference/web/virtual-background.md +197 -0
- package/knowledge-base/slices/conference/web/webinar-interaction.md +206 -0
- package/knowledge-base/slices/live/anchor-lifecycle.md +122 -0
- package/knowledge-base/slices/live/anchor-preview.md +90 -0
- package/knowledge-base/slices/live/anchor-room-config.md +104 -0
- package/knowledge-base/slices/live/audience-list.md +86 -0
- package/knowledge-base/slices/live/audience-manage.md +92 -0
- package/knowledge-base/slices/live/audience-watch.md +85 -0
- package/knowledge-base/slices/live/audio.md +116 -0
- package/knowledge-base/slices/live/barrage.md +88 -0
- package/knowledge-base/slices/live/beauty.md +99 -0
- package/knowledge-base/slices/live/coguest-apply.md +105 -0
- package/knowledge-base/slices/live/device-control.md +91 -0
- package/knowledge-base/slices/live/error-codes.md +167 -0
- package/knowledge-base/slices/live/gift.md +84 -0
- package/knowledge-base/slices/live/ios/.gitkeep +0 -0
- package/knowledge-base/slices/live/ios/anchor-lifecycle.md +313 -0
- package/knowledge-base/slices/live/ios/anchor-preview.md +228 -0
- package/knowledge-base/slices/live/ios/anchor-room-config.md +257 -0
- package/knowledge-base/slices/live/ios/audience-list.md +353 -0
- package/knowledge-base/slices/live/ios/audience-manage.md +381 -0
- package/knowledge-base/slices/live/ios/audience-watch.md +286 -0
- package/knowledge-base/slices/live/ios/audio.md +373 -0
- package/knowledge-base/slices/live/ios/barrage.md +285 -0
- package/knowledge-base/slices/live/ios/beauty.md +323 -0
- package/knowledge-base/slices/live/ios/coguest-apply.md +506 -0
- package/knowledge-base/slices/live/ios/device-control.md +286 -0
- package/knowledge-base/slices/live/ios/error-codes.md +270 -0
- package/knowledge-base/slices/live/ios/gift.md +315 -0
- package/knowledge-base/slices/live/ios/live-list.md +269 -0
- package/knowledge-base/slices/live/ios/login-auth.md +247 -0
- package/knowledge-base/slices/live/live-list.md +82 -0
- package/knowledge-base/slices/live/login-auth.md +78 -0
- package/package.json +34 -0
- package/skills/trtc/SKILL.md +326 -0
- package/skills/trtc/room-builder/SKILL.md +138 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/README.md +108 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/backend-contract.zh-CN.md +162 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/integration.zh-CN.md +154 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/theme.zh-CN.md +78 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/index.html +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/package.json +28 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/postcss.config.js +5 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/App.vue +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/ConsultationManagePanel.vue +838 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LanguageSwitch.vue +102 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LoadingSpinner.vue +6 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalAlert.vue +34 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalBusinessPanel.vue +148 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalButton.vue +49 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalConfirmDialog.vue +68 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalDataPanel.vue +196 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalRecordPanel.vue +270 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/PrescriptionPanel.vue +363 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/basic-info-config.ts +29 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.d.ts +4 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.js +2 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/runtime-config.ts +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/env.d.ts +32 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationChatPanel.vue +123 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationMembersPanel.vue +230 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationTranscriptionPanel.vue +135 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationVideoStage.vue +113 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/InviteDoctorDialog.vue +132 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/KickMemberConfirmDialog.vue +50 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/types.ts +77 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationChat.ts +97 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationDevices.ts +48 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationParticipants.ts +121 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationPermissions.ts +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/utils.ts +70 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/en-US/index.ts +553 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/index.ts +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/medicalTranslate.ts +85 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/state.ts +49 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/zh-CN/index.ts +463 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/main.ts +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/appointments.ts +96 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/users.ts +79 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/router/index.ts +63 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/index.ts +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/appointmentService.ts +77 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/authService.ts +38 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/launchContext.ts +31 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/userService.ts +35 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/appointmentService.ts +43 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/authService.ts +33 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/userService.ts +43 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/types.ts +135 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/shared/icons.ts +53 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/index.css +106 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/tailwind.css +3 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/theme.css +209 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/auth.ts +50 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/format.ts +24 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/navigation.ts +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/session.ts +28 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorConsultationView.vue +777 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorDashboardView.vue +678 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/LoginView.vue +441 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationFinishedView.vue +185 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationView.vue +1003 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientSelectDoctorView.vue +317 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientWaitingView.vue +454 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.json +21 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.node.json +8 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/vite.config.ts +17 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation//346/216/245/345/205/245/350/257/264/346/230/216.md +6 -0
- package/skills/trtc/room-builder/tools/render_ai_instructions.py +226 -0
- package/skills/trtc-apply/SKILL.md +97 -0
- package/skills/trtc-apply/guardrails/apply_lib/__init__.py +0 -0
- package/skills/trtc-apply/guardrails/apply_lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/trtc-apply/guardrails/apply_lib/__pycache__/rule_parser.cpython-313.pyc +0 -0
- package/skills/trtc-apply/guardrails/apply_lib/rule_parser.py +268 -0
- package/skills/trtc-docs/SKILL.md +207 -0
- package/skills/trtc-onboarding/SKILL.md +839 -0
- package/skills/trtc-onboarding/reference/path-a1-demo.md +103 -0
- package/skills/trtc-onboarding/reference/path-a2-integrate.md +693 -0
- package/skills/trtc-onboarding/reference/path-b-troubleshoot.md +115 -0
- package/skills/trtc-onboarding/reference/path-c-expand.md +43 -0
- package/skills/trtc-onboarding/reference/reporting-protocol.md +174 -0
- package/skills/trtc-onboarding/reference/supported-matrix.md +100 -0
- package/skills/trtc-onboarding/reference/usersig-handling.md +140 -0
- package/skills/trtc-search/SKILL.md +221 -0
- package/skills/trtc-topic/SKILL.md +638 -0
- package/skills/trtc-topic/guardrails/__pycache__/gate_slice_read.cpython-313.pyc +0 -0
- package/skills/trtc-topic/guardrails/__pycache__/gate_slice_write.cpython-313.pyc +0 -0
- package/skills/trtc-topic/guardrails/__pycache__/stop_require_apply_evidence.cpython-313.pyc +0 -0
- package/skills/trtc-topic/guardrails/gate_slice_read.py +133 -0
- package/skills/trtc-topic/guardrails/gate_slice_write.py +169 -0
- package/skills/trtc-topic/guardrails/stop_require_apply_evidence.py +97 -0
- package/skills/trtc-topic/references/execution-units.yaml +58 -0
- package/skills/trtc-topic/runtime/README.md +50 -0
- package/skills/trtc-topic/runtime/RUNTIME.md +128 -0
- package/skills/trtc-topic/runtime/lib/__init__.py +0 -0
- package/skills/trtc-topic/runtime/lib/platforms.py +194 -0
- package/skills/trtc-topic/runtime/package-lock.json +1211 -0
- package/skills/trtc-topic/runtime/package.json +13 -0
- package/skills/trtc-topic/runtime/telemetry-bridge.mjs +339 -0
- package/skills/trtc-topic/runtime/telemetry_collector.py +293 -0
- package/skills/trtc-topic/scripts/STATE-MACHINE-GUIDE.md +186 -0
- package/skills/trtc-topic/scripts/__pycache__/apply.cpython-313.pyc +0 -0
- package/skills/trtc-topic/scripts/apply.py +581 -0
- package/skills/trtc-topic/scripts/finalize_session.py +113 -0
- package/skills/trtc-topic/scripts/init_slice_queue.py +96 -0
- package/skills/trtc-topic/scripts/lib/__pycache__/state_machine.cpython-313.pyc +0 -0
- package/skills/trtc-topic/scripts/lib/state_machine.py +328 -0
- package/skills/trtc-topic/scripts/next_slice.py +137 -0
- package/skills/trtc-topic/tests/README.md +70 -0
- package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/conftest.py +72 -0
- package/skills/trtc-topic/tests/test_apply_cli.py +480 -0
- package/skills/trtc-topic/tests/test_end_to_end.py +305 -0
- package/skills/trtc-topic/tests/test_finalize_session.py +51 -0
- package/skills/trtc-topic/tests/test_gates.py +316 -0
- package/skills/trtc-topic/tests/test_session_resolver.py +260 -0
- package/skills/trtc-topic/tests/test_state_machine.py +414 -0
- package/skills/trtc-topic/tests/test_stop_require_apply.py +99 -0
- package/skills/trtc-topic/tests/test_topic_skill_invariants.py +130 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: live/gift
|
|
3
|
+
platform: ios
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 礼物 — iOS 实现
|
|
7
|
+
|
|
8
|
+
## 前置条件
|
|
9
|
+
|
|
10
|
+
**依赖安装(Podfile)**
|
|
11
|
+
```ruby
|
|
12
|
+
pod 'AtomicXCore', '~> 4.0'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**前置状态**:
|
|
16
|
+
- `LoginStore.shared.isLogin == true`(礼物功能依赖登录态)
|
|
17
|
+
- 已成功加入直播间(liveID 与进房 ID 一致)
|
|
18
|
+
- 服务端已配置礼物系统及扣费回调
|
|
19
|
+
|
|
20
|
+
## API 调用
|
|
21
|
+
|
|
22
|
+
```swift
|
|
23
|
+
// 创建礼物实例(与直播间绑定)
|
|
24
|
+
let giftStore = GiftStore.create(liveID: liveID)
|
|
25
|
+
|
|
26
|
+
// (可选)设置语言,须在 refreshUsableGifts 前调用
|
|
27
|
+
// ⚠️ 首个参数无标签(unnamed),直接传值
|
|
28
|
+
giftStore.setLanguage("zh-CN")
|
|
29
|
+
|
|
30
|
+
// 拉取礼物列表
|
|
31
|
+
giftStore.refreshUsableGifts(completion: CompletionClosure?)
|
|
32
|
+
// CompletionClosure = (Result<Void, ErrorInfo>) -> Void
|
|
33
|
+
|
|
34
|
+
// 发送礼物
|
|
35
|
+
// ⚠️ count 类型为 UInt,不是 Int
|
|
36
|
+
giftStore.sendGift(
|
|
37
|
+
giftID: String,
|
|
38
|
+
count: UInt,
|
|
39
|
+
completion: CompletionClosure?
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
// 订阅礼物事件
|
|
43
|
+
giftStore.giftEventPublisher // PassthroughSubject<GiftEvent, Never>
|
|
44
|
+
// GiftEvent 枚举:
|
|
45
|
+
// case .onReceiveGift(liveID: String, gift: Gift, count: UInt, sender: LiveUserInfo)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| 参数 | 类型 | 说明 |
|
|
49
|
+
|------|------|------|
|
|
50
|
+
| `liveID` | `String` | 直播间唯一标识 |
|
|
51
|
+
| `giftID` | `String` | 礼物唯一 ID,来自 `GiftState.usableGifts` |
|
|
52
|
+
| `count` | `UInt` | 发送数量,最小为 1 |
|
|
53
|
+
|
|
54
|
+
## 代码示例
|
|
55
|
+
|
|
56
|
+
### 完整礼物集成
|
|
57
|
+
|
|
58
|
+
```swift
|
|
59
|
+
import AtomicXCore
|
|
60
|
+
import Combine
|
|
61
|
+
|
|
62
|
+
final class GiftManager {
|
|
63
|
+
|
|
64
|
+
// MARK: - 属性
|
|
65
|
+
|
|
66
|
+
private let giftStore: GiftStore
|
|
67
|
+
private var cancellables = Set<AnyCancellable>()
|
|
68
|
+
|
|
69
|
+
/// 礼物分类列表(供 UI 面板使用)
|
|
70
|
+
@Published private(set) var giftCategories: [GiftCategory] = []
|
|
71
|
+
|
|
72
|
+
// MARK: - 初始化
|
|
73
|
+
|
|
74
|
+
init(liveID: String, language: String = "zh-CN") {
|
|
75
|
+
// 步骤1: 创建 GiftStore 实例
|
|
76
|
+
self.giftStore = GiftStore.create(liveID: liveID)
|
|
77
|
+
|
|
78
|
+
// 步骤2: 多语言场景先设置语言(首个参数无标签)
|
|
79
|
+
giftStore.setLanguage(language)
|
|
80
|
+
|
|
81
|
+
// 步骤3: 订阅礼物事件(进房后立即订阅,避免遗漏)
|
|
82
|
+
subscribeGiftEvents()
|
|
83
|
+
|
|
84
|
+
// 步骤4: 订阅礼物列表状态
|
|
85
|
+
subscribeGiftState()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// MARK: - 礼物列表
|
|
89
|
+
|
|
90
|
+
/// 拉取可用礼物列表(面板打开前调用)
|
|
91
|
+
func loadGifts(completion: ((Result<Void, ErrorInfo>) -> Void)? = nil) {
|
|
92
|
+
giftStore.refreshUsableGifts { result in
|
|
93
|
+
switch result {
|
|
94
|
+
case .success:
|
|
95
|
+
print("[Gift] 礼物列表拉取成功")
|
|
96
|
+
completion?(.success(()))
|
|
97
|
+
case .failure(let error):
|
|
98
|
+
print("[Gift] 礼物列表拉取失败 code=\(error.code) msg=\(error.message)")
|
|
99
|
+
completion?(.failure(error))
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private func subscribeGiftState() {
|
|
105
|
+
giftStore.state
|
|
106
|
+
.map(\.usableGifts)
|
|
107
|
+
.receive(on: DispatchQueue.main)
|
|
108
|
+
.assign(to: &$giftCategories)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// MARK: - 礼物事件订阅
|
|
112
|
+
|
|
113
|
+
private func subscribeGiftEvents() {
|
|
114
|
+
// 步骤5: 通过 giftEventPublisher 处理所有礼物 UI
|
|
115
|
+
giftStore.giftEventPublisher
|
|
116
|
+
.receive(on: DispatchQueue.main)
|
|
117
|
+
.sink { [weak self] event in
|
|
118
|
+
switch event {
|
|
119
|
+
case .onReceiveGift(let liveID, let gift, let count, let sender):
|
|
120
|
+
self?.handleReceiveGift(liveID: liveID, gift: gift, count: count, sender: sender)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
.store(in: &cancellables)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private func handleReceiveGift(liveID: String, gift: Gift, count: UInt, sender: LiveUserInfo) {
|
|
127
|
+
print("[Gift] 收到礼物: \(gift.name) ×\(count) from \(sender.userName)")
|
|
128
|
+
|
|
129
|
+
// 播放礼物动画
|
|
130
|
+
// GiftAnimationPlayer.shared.play(resourceURL: gift.resourceURL, ...)
|
|
131
|
+
|
|
132
|
+
// 在弹幕区插入礼物通知(通过 BarrageStore.appendLocalTip)
|
|
133
|
+
// 见 live/barrage slice
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// MARK: - 发送礼物
|
|
137
|
+
|
|
138
|
+
/// 发送礼物(从礼物面板调用)
|
|
139
|
+
/// - Parameter count: UInt 类型,最小值为 1
|
|
140
|
+
func sendGift(giftID: String,
|
|
141
|
+
count: UInt = 1,
|
|
142
|
+
completion: ((Result<Void, ErrorInfo>) -> Void)? = nil) {
|
|
143
|
+
// 步骤6: 发送礼物
|
|
144
|
+
giftStore.sendGift(giftID: giftID, count: count) { result in
|
|
145
|
+
switch result {
|
|
146
|
+
case .success:
|
|
147
|
+
// ✅ 成功后不在此处展示动画!
|
|
148
|
+
// 动画由 giftEventPublisher 统一驱动(发送方也会收到 onReceiveGift)
|
|
149
|
+
completion?(.success(()))
|
|
150
|
+
|
|
151
|
+
case .failure(let error):
|
|
152
|
+
// ❌ 仅在失败时处理
|
|
153
|
+
print("[Gift] 发送失败 code=\(error.code) msg=\(error.message)")
|
|
154
|
+
completion?(.failure(error))
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// MARK: - 资源清理
|
|
160
|
+
|
|
161
|
+
func cleanup() {
|
|
162
|
+
cancellables.removeAll()
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### 礼物面板示例
|
|
168
|
+
|
|
169
|
+
```swift
|
|
170
|
+
final class GiftPanelViewController: UIViewController {
|
|
171
|
+
|
|
172
|
+
private var giftManager: GiftManager!
|
|
173
|
+
private var cancellables = Set<AnyCancellable>()
|
|
174
|
+
|
|
175
|
+
@IBOutlet weak var collectionView: UICollectionView!
|
|
176
|
+
@IBOutlet weak var loadingIndicator: UIActivityIndicatorView!
|
|
177
|
+
|
|
178
|
+
override func viewDidLoad() {
|
|
179
|
+
super.viewDidLoad()
|
|
180
|
+
|
|
181
|
+
// 绑定礼物列表数据
|
|
182
|
+
giftManager.$giftCategories
|
|
183
|
+
.receive(on: DispatchQueue.main)
|
|
184
|
+
.sink { [weak self] categories in
|
|
185
|
+
guard !categories.isEmpty else { return }
|
|
186
|
+
self?.collectionView.reloadData()
|
|
187
|
+
self?.loadingIndicator.stopAnimating()
|
|
188
|
+
}
|
|
189
|
+
.store(in: &cancellables)
|
|
190
|
+
|
|
191
|
+
// 面板出现时拉取礼物列表
|
|
192
|
+
loadingIndicator.startAnimating()
|
|
193
|
+
giftManager.loadGifts { [weak self] result in
|
|
194
|
+
if case .failure(let error) = result {
|
|
195
|
+
self?.showErrorToast(message: error.message)
|
|
196
|
+
self?.loadingIndicator.stopAnimating()
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 用户点击礼物(count 为 UInt)
|
|
202
|
+
func didSelectGift(_ gift: Gift, count: UInt) {
|
|
203
|
+
giftManager.sendGift(giftID: gift.giftID, count: count) { [weak self] result in
|
|
204
|
+
DispatchQueue.main.async {
|
|
205
|
+
switch result {
|
|
206
|
+
case .success:
|
|
207
|
+
break // 动画由 giftEventPublisher 驱动,此处无需操作
|
|
208
|
+
case .failure(let error):
|
|
209
|
+
self?.handleSendError(error)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private func handleSendError(_ error: ErrorInfo) {
|
|
216
|
+
switch error.code {
|
|
217
|
+
case -4001:
|
|
218
|
+
showRechargeAlert() // 余额不足,引导充值
|
|
219
|
+
case -4002:
|
|
220
|
+
// 礼物不存在,刷新列表
|
|
221
|
+
giftManager.loadGifts()
|
|
222
|
+
default:
|
|
223
|
+
showErrorToast(message: error.message)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private func showRechargeAlert() {
|
|
228
|
+
let alert = UIAlertController(
|
|
229
|
+
title: "余额不足",
|
|
230
|
+
message: "当前金币不足,是否前往充值?",
|
|
231
|
+
preferredStyle: .alert
|
|
232
|
+
)
|
|
233
|
+
alert.addAction(UIAlertAction(title: "去充值", style: .default) { _ in
|
|
234
|
+
// 跳转充值页面
|
|
235
|
+
})
|
|
236
|
+
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
|
237
|
+
present(alert, animated: true)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### 多语言礼物列表
|
|
243
|
+
|
|
244
|
+
```swift
|
|
245
|
+
// 根据系统语言自动选择
|
|
246
|
+
func makeGiftManager(liveID: String) -> GiftManager {
|
|
247
|
+
let languageCode: String
|
|
248
|
+
if let preferred = Locale.preferredLanguages.first {
|
|
249
|
+
languageCode = preferred.hasPrefix("zh") ? "zh-CN" : "en"
|
|
250
|
+
} else {
|
|
251
|
+
languageCode = "zh-CN"
|
|
252
|
+
}
|
|
253
|
+
return GiftManager(liveID: liveID, language: languageCode)
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## 调用时序
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
进房成功
|
|
261
|
+
│
|
|
262
|
+
▼
|
|
263
|
+
GiftStore.create(liveID:) // 创建实例
|
|
264
|
+
│
|
|
265
|
+
├─ setLanguage("zh-CN") // (可选)多语言设置,首个参数无标签
|
|
266
|
+
│
|
|
267
|
+
▼
|
|
268
|
+
订阅 giftEventPublisher // ⚠️ 进房后立即订阅,避免遗漏礼物
|
|
269
|
+
│
|
|
270
|
+
├─ 用户打开礼物面板
|
|
271
|
+
│ │
|
|
272
|
+
│ ▼
|
|
273
|
+
│ refreshUsableGifts() // 拉取礼物分类+列表
|
|
274
|
+
│ │
|
|
275
|
+
│ ├─ .failure(ErrorInfo) → 展示 error.message,提供重试
|
|
276
|
+
│ └─ .success → GiftState.usableGifts 更新 → 渲染面板
|
|
277
|
+
│
|
|
278
|
+
├─ 用户点击发送礼物(count: UInt)
|
|
279
|
+
│ │
|
|
280
|
+
│ ▼
|
|
281
|
+
│ sendGift(giftID:count:)
|
|
282
|
+
│ ├─ .failure(code: -4001) → 引导充值
|
|
283
|
+
│ ├─ .failure(code: -4002) → 刷新礼物列表
|
|
284
|
+
│ └─ .success → (无需额外操作)
|
|
285
|
+
│
|
|
286
|
+
├─ 收到 giftEventPublisher 事件
|
|
287
|
+
│ │
|
|
288
|
+
│ ▼
|
|
289
|
+
│ .onReceiveGift(liveID:gift:count:sender:)
|
|
290
|
+
│ ├─ count 类型为 UInt
|
|
291
|
+
│ ├─ 播放礼物动画(gift.resourceURL)
|
|
292
|
+
│ └─ 弹幕区插入礼物提示
|
|
293
|
+
│
|
|
294
|
+
└─ 退出直播间
|
|
295
|
+
│
|
|
296
|
+
▼
|
|
297
|
+
cancellables.removeAll()
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## 平台特有注意事项
|
|
301
|
+
|
|
302
|
+
### 1. count 类型为 UInt
|
|
303
|
+
`sendGift` 的 `count` 参数及 `onReceiveGift` 事件的 `count` 都是 `UInt`,不是 `Int`。UI 层的计数变量应使用 `UInt` 或在传入时做显式转换。
|
|
304
|
+
|
|
305
|
+
### 2. setLanguage 参数无标签
|
|
306
|
+
`giftStore.setLanguage("zh-CN")` 中首个参数没有外部标签(unnamed),直接传字符串值。不要写 `giftStore.setLanguage(language: "zh-CN")`。
|
|
307
|
+
|
|
308
|
+
### 3. 礼物动画资源预加载
|
|
309
|
+
`Gift.resourceURL` 指向动画文件(如 SVGA、MP4)。建议在 `refreshUsableGifts` 成功后异步预下载高频礼物动画文件到本地缓存,避免发送时实时下载导致动画延迟。
|
|
310
|
+
|
|
311
|
+
### 4. 连击礼物(连续发送相同礼物)
|
|
312
|
+
iOS 上实现连击需在 UI 层维护连击计数和防抖 Timer(建议 800ms 间隔)。收到 `onReceiveGift` 后判断是否与上一条礼物来自同一发送者且礼物相同,若是则累计数量更新 UI,否则重新播放动画。
|
|
313
|
+
|
|
314
|
+
### 5. 内存管理
|
|
315
|
+
礼物动画(SVGA/Lottie)占用内存较高。建议同时最多播放 3 个礼物动画,超出时将旧动画排队等待完成后再播放,使用动画队列管理器控制并发数。
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: live/live-list
|
|
3
|
+
platform: ios
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 直播列表 — iOS 实现
|
|
7
|
+
|
|
8
|
+
## 前置条件
|
|
9
|
+
|
|
10
|
+
**依赖安装(Podfile)**
|
|
11
|
+
```ruby
|
|
12
|
+
pod 'AtomicXCore', '~> 4.0'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**最低系统要求**:iOS 13.0+,Xcode 14.0+
|
|
16
|
+
|
|
17
|
+
**前置登录**:必须在 `LoginStore.shared.login` 成功回调后才可调用 `fetchLiveList`。
|
|
18
|
+
|
|
19
|
+
## API 调用(真实签名)
|
|
20
|
+
|
|
21
|
+
```swift
|
|
22
|
+
// 拉取直播列表(分页)
|
|
23
|
+
// ⚠️ completion 是 CompletionClosure(Result<Void, ErrorInfo>),列表通过 state 读取
|
|
24
|
+
LiveListStore.shared.fetchLiveList(
|
|
25
|
+
cursor: String, // 首次传 "",后续传 liveListCursor
|
|
26
|
+
count: Int, // 每页数量,推荐 20
|
|
27
|
+
completion: CompletionClosure? // (Result<Void, ErrorInfo>) -> Void
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
// 读取状态快照(Combine)
|
|
31
|
+
LiveListStore.shared.state // StatePublisher<LiveListState>
|
|
32
|
+
|
|
33
|
+
// 订阅异步事件
|
|
34
|
+
LiveListStore.shared.liveListEventPublisher // PassthroughSubject<LiveListEvent, Never>
|
|
35
|
+
|
|
36
|
+
// 加入直播间(观众)
|
|
37
|
+
// ⚠️ completion 返回 LiveInfo(不是 Void)
|
|
38
|
+
LiveListStore.shared.joinLive(liveID: String,
|
|
39
|
+
completion: LiveInfoCompletionClosure?)
|
|
40
|
+
// LiveInfoCompletionClosure = (Result<LiveInfo, ErrorInfo>) -> Void
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**LiveListState 关键字段**
|
|
44
|
+
```swift
|
|
45
|
+
struct LiveListState {
|
|
46
|
+
var liveList: [LiveInfo] // 当前已拉取的直播列表
|
|
47
|
+
var liveListCursor: String // 下一页游标;"" 表示已到末页
|
|
48
|
+
var currentLive: LiveInfo // 当前所在直播间的信息
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**LiveListEvent 完整签名**
|
|
53
|
+
```swift
|
|
54
|
+
enum LiveListEvent {
|
|
55
|
+
case onLiveEnded(liveID: String, reason: LiveEndedReason, message: String)
|
|
56
|
+
case onKickedOutOfLive(liveID: String, reason: LiveKickedOutReason, message: String)
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
| 参数 | 类型 | 说明 |
|
|
61
|
+
|------|------|------|
|
|
62
|
+
| `cursor` | `String` | 分页游标;首次传 `""`,末页时 `liveListCursor` 返回 `""` |
|
|
63
|
+
| `count` | `Int` | 单次返回条数,建议 20,最大 50 |
|
|
64
|
+
|
|
65
|
+
**LiveInfo 关键字段**
|
|
66
|
+
|
|
67
|
+
| 字段 | 类型 | 说明 |
|
|
68
|
+
|------|------|------|
|
|
69
|
+
| `liveID` | `String` | 直播间唯一 ID |
|
|
70
|
+
| `liveName` | `String` | 直播间名称 |
|
|
71
|
+
| `coverURL` | `String` | 封面 URL |
|
|
72
|
+
| `liveOwner` | `LiveUserInfo` | 主播信息 |
|
|
73
|
+
| `seatTemplate` | `SeatLayoutTemplate` | 座位布局模板 |
|
|
74
|
+
| `categoryList` | `[NSNumber]` | 分类标签(NSNumber,不是 String) |
|
|
75
|
+
| `metaData` | `[String: String]` | 自定义扩展数据 |
|
|
76
|
+
| `totalViewerCount` | `Int` | 当前观看人数 |
|
|
77
|
+
| `isEmpty` | `Bool` | 直播间是否为空(可用于过滤) |
|
|
78
|
+
|
|
79
|
+
## 代码示例
|
|
80
|
+
|
|
81
|
+
```swift
|
|
82
|
+
import AtomicXCore
|
|
83
|
+
import Combine
|
|
84
|
+
|
|
85
|
+
var cancellables = Set<AnyCancellable>()
|
|
86
|
+
|
|
87
|
+
// MARK: - 分页拉取直播列表
|
|
88
|
+
// ⚠️ fetchLiveList completion 是 Result<Void, ErrorInfo>
|
|
89
|
+
// 列表数据通过 state 读取,不是通过 completion 参数返回
|
|
90
|
+
|
|
91
|
+
func fetchFirstPage() {
|
|
92
|
+
LiveListStore.shared.fetchLiveList(cursor: "", count: 20) { result in
|
|
93
|
+
switch result {
|
|
94
|
+
case .success:
|
|
95
|
+
// ✅ 拉取成功后,从 state 读取列表
|
|
96
|
+
// (state 是 StatePublisher,可订阅也可直接读当前值)
|
|
97
|
+
observeLiveListState()
|
|
98
|
+
|
|
99
|
+
case .failure(let errorInfo):
|
|
100
|
+
print("[LiveList] 拉取失败, code: \(errorInfo.code), msg: \(errorInfo.message)")
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func fetchMorePages() {
|
|
106
|
+
// 通过 state 读取当前 cursor
|
|
107
|
+
// (StatePublisher 读当前值的方式取决于具体 SDK 实现,此处以 .value 示意)
|
|
108
|
+
// TODO: 待验证 — StatePublisher 读取当前值的具体方式
|
|
109
|
+
let currentCursor = "..." // 从 state.liveListCursor 获取
|
|
110
|
+
guard !currentCursor.isEmpty else {
|
|
111
|
+
print("[LiveList] 已到末页")
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
LiveListStore.shared.fetchLiveList(cursor: currentCursor, count: 20) { result in
|
|
116
|
+
switch result {
|
|
117
|
+
case .success:
|
|
118
|
+
print("[LiveList] 更多页拉取成功")
|
|
119
|
+
case .failure(let errorInfo):
|
|
120
|
+
print("[LiveList] 拉取更多失败, code: \(errorInfo.code)")
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// MARK: - 订阅状态(Combine)
|
|
126
|
+
|
|
127
|
+
func observeLiveListState() {
|
|
128
|
+
LiveListStore.shared.state
|
|
129
|
+
.receive(on: DispatchQueue.main)
|
|
130
|
+
.sink { liveListState in
|
|
131
|
+
let list = liveListState.liveList // [LiveInfo]
|
|
132
|
+
let cursor = liveListState.liveListCursor // String("" = 末页)
|
|
133
|
+
print("[LiveList] 当前列表数量: \(list.count), 下一页 cursor: \(cursor)")
|
|
134
|
+
// 刷新 UI
|
|
135
|
+
}
|
|
136
|
+
.store(in: &cancellables)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// MARK: - 订阅直播事件
|
|
140
|
+
|
|
141
|
+
func subscribeLiveListEvents() {
|
|
142
|
+
LiveListStore.shared.liveListEventPublisher
|
|
143
|
+
.receive(on: DispatchQueue.main)
|
|
144
|
+
.sink { event in
|
|
145
|
+
switch event {
|
|
146
|
+
// ⚠️ onLiveEnded 三个关联值
|
|
147
|
+
case .onLiveEnded(let liveID, _, _):
|
|
148
|
+
print("[LiveList] 直播 \(liveID) 已结束,从列表移除")
|
|
149
|
+
|
|
150
|
+
// ⚠️ onKickedOutOfLive 三个关联值
|
|
151
|
+
case .onKickedOutOfLive(let liveID, let reason, let message):
|
|
152
|
+
print("[LiveList] 被踢出 \(liveID), reason: \(reason), msg: \(message)")
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
.store(in: &cancellables)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// MARK: - 加入直播间(joinLive 返回 LiveInfo,不是 Void)
|
|
159
|
+
|
|
160
|
+
func joinLive(liveID: String) {
|
|
161
|
+
// ⚠️ joinLive completion 是 LiveInfoCompletionClosure,返回 LiveInfo
|
|
162
|
+
LiveListStore.shared.joinLive(liveID: liveID) { result in
|
|
163
|
+
switch result {
|
|
164
|
+
case .success(let liveInfo):
|
|
165
|
+
// 成功时回调携带完整 LiveInfo
|
|
166
|
+
print("[LiveList] 进房成功")
|
|
167
|
+
print("[LiveList] 直播间名称: \(liveInfo.liveName)")
|
|
168
|
+
print("[LiveList] 主播: \(liveInfo.liveOwner.userID)")
|
|
169
|
+
// 进房成功后启用弹幕/礼物等功能
|
|
170
|
+
|
|
171
|
+
case .failure(let errorInfo):
|
|
172
|
+
print("[LiveList] 进房失败, code: \(errorInfo.code)")
|
|
173
|
+
handleJoinError(errorInfo)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// MARK: - 进房错误处理
|
|
179
|
+
|
|
180
|
+
func handleJoinError(_ errorInfo: ErrorInfo) {
|
|
181
|
+
switch errorInfo.code {
|
|
182
|
+
case -1002: print("请先登录")
|
|
183
|
+
case -2001: print("直播间不存在或已结束")
|
|
184
|
+
default: print("进房失败(code: \(errorInfo.code)): \(errorInfo.message)")
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## 调用时序
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
LoginStore.login 成功
|
|
193
|
+
│
|
|
194
|
+
▼
|
|
195
|
+
LiveListStore.shared.fetchLiveList(cursor: "", count: 20) { result in ... }
|
|
196
|
+
│
|
|
197
|
+
├─ .failure(errorInfo) → 检查登录态 / 网络
|
|
198
|
+
│
|
|
199
|
+
└─ .success
|
|
200
|
+
│
|
|
201
|
+
▼
|
|
202
|
+
LiveListStore.shared.state 订阅
|
|
203
|
+
读取 liveListState.liveList ← [LiveInfo]
|
|
204
|
+
读取 liveListState.liveListCursor ← 下一页游标
|
|
205
|
+
│
|
|
206
|
+
├─ 渲染列表(reloadData)
|
|
207
|
+
│
|
|
208
|
+
▼
|
|
209
|
+
用户滑动到底部 → fetchLiveList(cursor: cursor, count: 20)
|
|
210
|
+
│
|
|
211
|
+
└─ .success → state 更新 liveList 和 liveListCursor
|
|
212
|
+
│
|
|
213
|
+
▼
|
|
214
|
+
用户点击某个直播间
|
|
215
|
+
│
|
|
216
|
+
▼
|
|
217
|
+
LiveListStore.shared.joinLive(liveID:) { result in ... }
|
|
218
|
+
│
|
|
219
|
+
├─ .failure(errorInfo) → 展示错误
|
|
220
|
+
└─ .success(liveInfo) → 进入直播间(启用弹幕/礼物等)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## 平台特有注意事项
|
|
224
|
+
|
|
225
|
+
### 1. fetchLiveList completion 是 `Result<Void, ErrorInfo>`,列表从 state 读取
|
|
226
|
+
`fetchLiveList` 的 completion 只表示「请求是否成功」,实际列表数据通过 `LiveListStore.shared.state` 订阅获取:
|
|
227
|
+
```swift
|
|
228
|
+
// ✅ 正确流程
|
|
229
|
+
LiveListStore.shared.fetchLiveList(cursor: "", count: 20) { result in
|
|
230
|
+
if case .success = result {
|
|
231
|
+
// 从 state 读取列表
|
|
232
|
+
subscribeToState()
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ❌ 错误:completion 没有 list 参数
|
|
237
|
+
LiveListStore.shared.fetchLiveList(cursor: "", count: 20) { result in
|
|
238
|
+
if case .success(let list) = result { ... } // 编译错误
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### 2. joinLive completion 返回 LiveInfo,不是 Void
|
|
243
|
+
`joinLive` 的回调类型是 `LiveInfoCompletionClosure = (Result<LiveInfo, ErrorInfo>) -> Void`,成功时携带直播间完整信息:
|
|
244
|
+
```swift
|
|
245
|
+
case .success(let liveInfo): // ✅ liveInfo: LiveInfo
|
|
246
|
+
let ownerID = liveInfo.liveOwner.userID
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 3. LiveListEvent 关联值有三个字段
|
|
250
|
+
```swift
|
|
251
|
+
// ✅ 正确(三个关联值)
|
|
252
|
+
case .onLiveEnded(let liveID, let reason, let message):
|
|
253
|
+
case .onKickedOutOfLive(let liveID, let reason, let message):
|
|
254
|
+
|
|
255
|
+
// ❌ 错误(缺少 reason 和 message)
|
|
256
|
+
case .onLiveEnded(let liveID):
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### 4. categoryList 类型是 `[NSNumber]`,不是 `[String]`
|
|
260
|
+
```swift
|
|
261
|
+
// ✅ 正确
|
|
262
|
+
let categories: [NSNumber] = liveInfo.categoryList
|
|
263
|
+
|
|
264
|
+
// ❌ 错误
|
|
265
|
+
let categories: [String] = liveInfo.categoryList // 类型不匹配
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### 5. 内存管理:Cell 离屏必须退出直播间
|
|
269
|
+
iOS UICollectionView 会缓存离屏 Cell,若不在 `didEndDisplaying` 中调用 `leaveLive`,旧 Cell 的 `LiveCoreView` 会继续解码、占用解码器硬件资源,在直播列表页快速滑动时极易触发内存警告甚至 OOM。
|