Compare commits
4 Commits
main
...
BlindBoxVi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55255bf0f8 | ||
|
|
b1cd957d0c | ||
|
|
36b95abc37 | ||
|
|
df32ea71bb |
@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "e8f130fe30ac6cdc940ef06ee1e8535e9f46ffee6aeead1722b9525562f6ce08",
|
||||
"originHash" : "7ea295cc5e3eb8ef644b89ce2b47a7600994b67c8582ee354b643cd63250740d",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "alamofire",
|
||||
@ -9,6 +9,51 @@
|
||||
"revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5",
|
||||
"version" : "5.10.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "cocoalumberjack",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git",
|
||||
"state" : {
|
||||
"revision" : "a9ed4b6f9bdedce7d77046f43adfb8ce1fd54114",
|
||||
"version" : "3.9.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "lottie-spm",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/airbnb/lottie-spm.git",
|
||||
"state" : {
|
||||
"revision" : "04f2fd18cc9404a0a0917265a449002674f24ec9",
|
||||
"version" : "4.5.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "svgkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SVGKit/SVGKit.git",
|
||||
"state" : {
|
||||
"revision" : "58152b9f7c85eab239160b36ffdfd364aa43d666",
|
||||
"version" : "3.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-log",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-log",
|
||||
"state" : {
|
||||
"revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2",
|
||||
"version" : "1.6.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "waterfallgrid",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/paololeonardi/WaterfallGrid.git",
|
||||
"state" : {
|
||||
"revision" : "c7c08652c3540adf8e48409c351879b4caea7e89",
|
||||
"version" : "1.1.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<svg width="106" height="66" viewBox="0 0 106 66" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.666667 60C0.666667 62.9455 3.05448 65.3333 6 65.3333C8.94552 65.3333 11.3333 62.9455 11.3333 60C11.3333 57.0545 8.94552 54.6667 6 54.6667C3.05448 54.6667 0.666667 57.0545 0.666667 60ZM22.2802 31.0204L23.152 31.5102L22.2802 31.0204ZM97.5663 30V29H24.0239V30V31H97.5663V30ZM22.2802 31.0204L21.4083 30.5306L5.12816 59.5102L6 60L6.87184 60.4898L23.152 31.5102L22.2802 31.0204ZM24.0239 30V29C22.9395 29 21.9395 29.5852 21.4083 30.5306L22.2802 31.0204L23.152 31.5102C23.3291 31.1951 23.6624 31 24.0239 31V30Z" fill="black"/>
|
||||
<rect x="16.8433" width="89.1566" height="30" rx="15" fill="black"/>
|
||||
<svg width="90" height="66" viewBox="0 0 90 66" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.666667 60C0.666667 62.9455 3.05448 65.3333 6 65.3333C8.94552 65.3333 11.3333 62.9455 11.3333 60C11.3333 57.0545 8.94552 54.6667 6 54.6667C3.05448 54.6667 0.666667 57.0545 0.666667 60ZM28.9006 28.8162L29.7066 29.4081L28.9006 28.8162ZM75.5 28V27H30.5126V28V29H75.5V28ZM28.9006 28.8162L28.0946 28.2243L5.194 59.4081L6 60L6.806 60.5919L29.7066 29.4081L28.9006 28.8162ZM30.5126 28V27C29.5577 27 28.6598 27.4546 28.0946 28.2243L28.9006 28.8162L29.7066 29.4081C29.895 29.1515 30.1943 29 30.5126 29V28Z" fill="black"/>
|
||||
<rect x="15.5" width="74" height="30" rx="15" fill="black"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 705 B After Width: | Height: | Size: 687 B |
@ -74,7 +74,7 @@ struct BlindBoxView: View {
|
||||
case all
|
||||
}
|
||||
// 盲盒列表
|
||||
struct BlindList: Codable, Identifiable {
|
||||
struct BlindList: Codable, Identifiable, Equatable, Hashable {
|
||||
let id: Int64
|
||||
let boxCode: String
|
||||
let userId: Int64
|
||||
@ -104,6 +104,40 @@ struct BlindBoxView: View {
|
||||
case coverFileId = "cover_file_id"
|
||||
case description
|
||||
}
|
||||
|
||||
// Implement Equatable
|
||||
static func == (lhs: BlindBoxView.BlindList, rhs: BlindBoxView.BlindList) -> Bool {
|
||||
return lhs.id == rhs.id &&
|
||||
lhs.boxCode == rhs.boxCode &&
|
||||
lhs.userId == rhs.userId &&
|
||||
lhs.name == rhs.name &&
|
||||
lhs.boxType == rhs.boxType &&
|
||||
lhs.features == rhs.features &&
|
||||
lhs.resultFileId == rhs.resultFileId &&
|
||||
lhs.status == rhs.status &&
|
||||
lhs.workflowInstanceId == rhs.workflowInstanceId &&
|
||||
lhs.videoGenerateTime == rhs.videoGenerateTime &&
|
||||
lhs.createTime == rhs.createTime &&
|
||||
lhs.coverFileId == rhs.coverFileId &&
|
||||
lhs.description == rhs.description
|
||||
}
|
||||
|
||||
// Implement Hashable
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
hasher.combine(boxCode)
|
||||
hasher.combine(userId)
|
||||
hasher.combine(name)
|
||||
hasher.combine(boxType)
|
||||
hasher.combine(features)
|
||||
hasher.combine(resultFileId)
|
||||
hasher.combine(status)
|
||||
hasher.combine(workflowInstanceId)
|
||||
hasher.combine(videoGenerateTime)
|
||||
hasher.combine(createTime)
|
||||
hasher.combine(coverFileId)
|
||||
hasher.combine(description)
|
||||
}
|
||||
}
|
||||
// 盲盒数量
|
||||
struct BlindCount: Codable {
|
||||
@ -114,22 +148,35 @@ struct BlindBoxView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BlindBox Response Model
|
||||
// MARK: - API Response Wrapper
|
||||
struct APIResponse<T: Codable>: Codable {
|
||||
let code: Int
|
||||
let data: T
|
||||
let message: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case code
|
||||
case data
|
||||
case message
|
||||
}
|
||||
}
|
||||
|
||||
struct BlindBoxData: Codable {
|
||||
// MARK: - BlindBox Response Model
|
||||
struct BlindBoxData: Codable, Equatable {
|
||||
let id: Int64
|
||||
let boxCode: String
|
||||
let userId: Int64
|
||||
let name: String
|
||||
let boxType: String
|
||||
let features: String?
|
||||
let url: String?
|
||||
let resultFileId: Int64?
|
||||
let status: String
|
||||
let workflowInstanceId: String?
|
||||
// 视频生成时间
|
||||
let workflowInstanceId: String
|
||||
let videoGenerateTime: String?
|
||||
let createTime: String
|
||||
let description: String?
|
||||
let coverFileId: Int64?
|
||||
let url: String?
|
||||
let description: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
@ -138,29 +185,59 @@ struct BlindBoxView: View {
|
||||
case name
|
||||
case boxType = "box_type"
|
||||
case features
|
||||
case url
|
||||
case resultFileId = "result_file_id"
|
||||
case status
|
||||
case workflowInstanceId = "workflow_instance_id"
|
||||
case videoGenerateTime = "video_generate_time"
|
||||
case createTime = "create_time"
|
||||
case coverFileId = "cover_file_id"
|
||||
case url
|
||||
case description
|
||||
}
|
||||
|
||||
init(id: Int64, boxCode: String, userId: Int64, name: String, boxType: String, features: String?, url: String?, status: String, workflowInstanceId: String?, videoGenerateTime: String?, createTime: String, description: String?) {
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// Required fields with default values
|
||||
id = try container.decode(Int64.self, forKey: .id)
|
||||
boxCode = try container.decodeIfPresent(String.self, forKey: .boxCode) ?? ""
|
||||
userId = try container.decode(Int64.self, forKey: .userId)
|
||||
name = try container.decodeIfPresent(String.self, forKey: .name) ?? ""
|
||||
boxType = try container.decode(String.self, forKey: .boxType)
|
||||
status = try container.decode(String.self, forKey: .status)
|
||||
workflowInstanceId = try container.decode(String.self, forKey: .workflowInstanceId)
|
||||
createTime = try container.decode(String.self, forKey: .createTime)
|
||||
|
||||
// Optional fields
|
||||
features = try container.decodeIfPresent(String.self, forKey: .features)
|
||||
resultFileId = try container.decodeIfPresent(Int64.self, forKey: .resultFileId)
|
||||
videoGenerateTime = try container.decodeIfPresent(String.self, forKey: .videoGenerateTime)
|
||||
coverFileId = try container.decodeIfPresent(Int64.self, forKey: .coverFileId)
|
||||
url = try container.decodeIfPresent(String.self, forKey: .url)
|
||||
description = try container.decodeIfPresent(String.self, forKey: .description) ?? ""
|
||||
}
|
||||
|
||||
// Initializer for creating instances manually
|
||||
init(id: Int64, boxCode: String, userId: Int64, name: String, boxType: String, features: String?,
|
||||
resultFileId: Int64?, status: String, workflowInstanceId: String, videoGenerateTime: String?,
|
||||
createTime: String, coverFileId: Int64?, url: String?, description: String) {
|
||||
self.id = id
|
||||
self.boxCode = boxCode
|
||||
self.userId = userId
|
||||
self.name = name
|
||||
self.boxType = boxType
|
||||
self.features = features
|
||||
self.url = url
|
||||
self.resultFileId = resultFileId
|
||||
self.status = status
|
||||
self.workflowInstanceId = workflowInstanceId
|
||||
self.videoGenerateTime = videoGenerateTime
|
||||
self.createTime = createTime
|
||||
self.coverFileId = coverFileId
|
||||
self.url = url
|
||||
self.description = description
|
||||
}
|
||||
|
||||
// Initializer for creating from BlindList
|
||||
init(from listItem: BlindList) {
|
||||
self.init(
|
||||
id: listItem.id,
|
||||
@ -169,17 +246,28 @@ struct BlindBoxView: View {
|
||||
name: listItem.name,
|
||||
boxType: listItem.boxType,
|
||||
features: listItem.features,
|
||||
url: nil,
|
||||
resultFileId: listItem.resultFileId,
|
||||
status: listItem.status,
|
||||
workflowInstanceId: listItem.workflowInstanceId,
|
||||
workflowInstanceId: listItem.workflowInstanceId ?? "",
|
||||
videoGenerateTime: listItem.videoGenerateTime,
|
||||
createTime: listItem.createTime,
|
||||
coverFileId: listItem.coverFileId,
|
||||
url: nil,
|
||||
description: listItem.description
|
||||
)
|
||||
}
|
||||
|
||||
// Equatable conformance
|
||||
static func == (lhs: BlindBoxData, rhs: BlindBoxData) -> Bool {
|
||||
return lhs.id == rhs.id &&
|
||||
lhs.workflowInstanceId == rhs.workflowInstanceId &&
|
||||
lhs.status == rhs.status
|
||||
}
|
||||
}
|
||||
|
||||
let mediaType: BlindBoxMediaType
|
||||
private var ids: [String]?
|
||||
@State private var blindBoxItems: [BlindList]?
|
||||
@State private var showModal = false // 控制用户资料弹窗显示
|
||||
@State private var showSettings = false // 控制设置页面显示
|
||||
@State private var isMember = false // 是否是会员
|
||||
@ -217,8 +305,9 @@ struct BlindBoxView: View {
|
||||
// 查询数据 - 简单查询
|
||||
@Query private var login: [Login]
|
||||
|
||||
init(mediaType: BlindBoxMediaType) {
|
||||
init(mediaType: BlindBoxMediaType, ids: [String]? = nil) {
|
||||
self.mediaType = mediaType
|
||||
self.ids = ids
|
||||
}
|
||||
|
||||
// 倒计时
|
||||
@ -313,6 +402,8 @@ struct BlindBoxView: View {
|
||||
// 如果列表为空数组 设置盲盒状态为none
|
||||
if self.blindList.isEmpty {
|
||||
self.animationPhase = .none
|
||||
}else{
|
||||
self.animationPhase = .loading
|
||||
}
|
||||
print("✅ 成功获取 \(self.blindList.count) 个盲盒")
|
||||
case .failure(let error):
|
||||
@ -343,9 +434,19 @@ struct BlindBoxView: View {
|
||||
return
|
||||
}
|
||||
|
||||
// 准备参数
|
||||
var parameters: [String: Any] = ["box_type": currentBoxType=="Video" ? "Second" : "First"]
|
||||
|
||||
// 如果有传递的盲盒项,添加到参数中
|
||||
if let ids = ids, !ids.isEmpty {
|
||||
let fileIds = ids.compactMap { Int64($0) } // Convert string to Int64
|
||||
print("❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️material_ids: \(fileIds)")
|
||||
parameters["material_ids"] = fileIds
|
||||
}
|
||||
|
||||
NetworkService.shared.postWithToken(
|
||||
path: "/blind_box/generate/mock",
|
||||
parameters: ["box_type": currentBoxType]
|
||||
path: "/blind_box/generate",
|
||||
parameters: parameters
|
||||
) { (result: Result<APIResponse<BlindBoxData>, NetworkError>) in
|
||||
DispatchQueue.main.async {
|
||||
switch result {
|
||||
@ -534,7 +635,7 @@ struct BlindBoxView: View {
|
||||
default:
|
||||
// 其他状态不处理
|
||||
withAnimation {
|
||||
self.animationPhase = .ready
|
||||
self.animationPhase = .loading
|
||||
}
|
||||
break
|
||||
}
|
||||
@ -645,7 +746,7 @@ struct BlindBoxView: View {
|
||||
Button(action: showUserProfile) {
|
||||
SVGImage(svgName: "User")
|
||||
.frame(width: 24, height: 24)
|
||||
.padding(13) // Increases tap area while keeping visual size
|
||||
.padding(.trailing, 13) // Increases tap area while keeping visual size
|
||||
.contentShape(Rectangle()) // Makes the padded area tappable
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle()) // Prevents the button from affecting the layout
|
||||
@ -686,18 +787,25 @@ struct BlindBoxView: View {
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(16)
|
||||
}
|
||||
.padding(.trailing)
|
||||
.fullScreenCover(isPresented: $showLogin) {
|
||||
LoginView()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 20)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
// 标题
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Hi! Click And")
|
||||
Text("Open Your First Box~")
|
||||
HStack {
|
||||
Text("Open Your")
|
||||
if mediaType == .video {
|
||||
Text("Second")
|
||||
} else if mediaType == .image {
|
||||
Text("First")
|
||||
}
|
||||
Text("Box~")
|
||||
}
|
||||
}
|
||||
.font(Typography.font(for: .smallLargeTitle))
|
||||
.fontWeight(.bold)
|
||||
@ -809,7 +917,7 @@ struct BlindBoxView: View {
|
||||
.frame(width: 300, height: 300)
|
||||
|
||||
case .none:
|
||||
SVGImage(svgName: "BlindNone")
|
||||
SVGImageHtml(svgName: "BlindNone")
|
||||
.frame(width: 300, height: 300)
|
||||
}
|
||||
}
|
||||
@ -839,9 +947,9 @@ struct BlindBoxView: View {
|
||||
maxHeight: UIScreen.main.bounds.height * 0.65
|
||||
)
|
||||
.opacity(showScalingOverlay ? 0 : 1)
|
||||
.animation(.easeOut(duration: 1.5), value: showScalingOverlay)
|
||||
.animation(.easeOut(duration: 0.5), value: showScalingOverlay)
|
||||
.offset(y: showScalingOverlay ? -100 : 0)
|
||||
.animation(.easeInOut(duration: 1.5), value: showScalingOverlay)
|
||||
.animation(.easeInOut(duration: 0.5), value: showScalingOverlay)
|
||||
// 打开
|
||||
if mediaType == .all {
|
||||
Button(action: {
|
||||
|
||||
@ -7,7 +7,7 @@ enum AppRoute: Hashable {
|
||||
case feedbackView
|
||||
case feedbackDetail(type: FeedbackView.FeedbackType)
|
||||
case mediaUpload
|
||||
case blindBox(mediaType: BlindBoxView.BlindBoxMediaType)
|
||||
case blindBox(mediaType: BlindBoxView.BlindBoxMediaType, ids: [String]? = nil)
|
||||
case blindOutcome(media: MediaType, time: String? = nil, description: String? = nil)
|
||||
case memories
|
||||
case subscribe
|
||||
@ -16,6 +16,7 @@ enum AppRoute: Hashable {
|
||||
case about
|
||||
case permissionManagement
|
||||
case feedback
|
||||
case blindList
|
||||
|
||||
@ViewBuilder
|
||||
var view: some View {
|
||||
@ -30,8 +31,8 @@ enum AppRoute: Hashable {
|
||||
FeedbackDetailView(feedbackType: type)
|
||||
case .mediaUpload:
|
||||
MediaUploadView()
|
||||
case .blindBox(let mediaType):
|
||||
BlindBoxView(mediaType: mediaType)
|
||||
case .blindBox(let mediaType, let ids):
|
||||
BlindBoxView(mediaType: mediaType, ids: ids)
|
||||
case .blindOutcome(let media, let time, let description):
|
||||
BlindOutcomeView(media: media, time: time, description: description)
|
||||
case .memories:
|
||||
@ -48,6 +49,8 @@ enum AppRoute: Hashable {
|
||||
PermissionManagementView()
|
||||
case .feedback:
|
||||
FeedbackView()
|
||||
case .blindList:
|
||||
BlindListView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,7 +66,9 @@ struct BlindOutcomeView: View {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.white)
|
||||
.shadow(color: Color.black.opacity(0.1), radius: 8, x: 0, y: 2)
|
||||
|
||||
Spacer()
|
||||
.frame(height: 16)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
switch media {
|
||||
case .image(let uiImage):
|
||||
@ -75,7 +77,6 @@ struct BlindOutcomeView: View {
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.cornerRadius(10)
|
||||
.padding(4)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
isFullscreen.toggle()
|
||||
@ -84,7 +85,7 @@ struct BlindOutcomeView: View {
|
||||
|
||||
case .video(let url, _):
|
||||
VideoPlayerView(url: url, isPlaying: $isPlaying, player: $player)
|
||||
.frame(width: UIScreen.main.bounds.width - 40)
|
||||
.frame(width: UIScreen.main.bounds.width)
|
||||
.background(Color.clear)
|
||||
.cornerRadius(10)
|
||||
.clipped()
|
||||
|
||||
@ -154,10 +154,10 @@ struct JoinModal: View {
|
||||
.font(Typography.font(for: .body, family: .quicksandBold))
|
||||
}
|
||||
.foregroundColor(.themeTextMessageMain)
|
||||
.padding(.vertical, 12)
|
||||
.padding(.vertical, 16)
|
||||
.padding(.horizontal, 30)
|
||||
.background(Color.themePrimary)
|
||||
.cornerRadius(20)
|
||||
.cornerRadius(32)
|
||||
}
|
||||
.padding(.top, 16)
|
||||
// 协议条款
|
||||
|
||||
@ -196,27 +196,27 @@ struct UserProfileModal: View {
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
|
||||
// Box
|
||||
// Button(action: {
|
||||
// Router.shared.navigate(to: .mediaUpload)
|
||||
// }) {
|
||||
// HStack(spacing: 16) {
|
||||
// SVGImage(svgName: "Box")
|
||||
// .foregroundColor(.orange)
|
||||
// .frame(width: 20, height: 20)
|
||||
//Box
|
||||
Button(action: {
|
||||
Router.shared.navigate(to: .blindList)
|
||||
}) {
|
||||
HStack(spacing: 16) {
|
||||
SVGImage(svgName: "Box")
|
||||
.foregroundColor(.orange)
|
||||
.frame(width: 20, height: 20)
|
||||
|
||||
// Text("My Blind Box")
|
||||
// .font(Typography.font(for: .body))
|
||||
// .fontWeight(.bold)
|
||||
// .foregroundColor(.themeTextMessageMain)
|
||||
Text("My Blind Box")
|
||||
.font(Typography.font(for: .body))
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.themeTextMessageMain)
|
||||
|
||||
// Spacer()
|
||||
// }
|
||||
// .padding()
|
||||
// .cornerRadius(10)
|
||||
// .contentShape(Rectangle())
|
||||
// }
|
||||
// .buttonStyle(PlainButtonStyle())
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.cornerRadius(10)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
|
||||
// setting
|
||||
Button(action: {
|
||||
|
||||
@ -39,7 +39,7 @@ struct CreditsInfoCard: View {
|
||||
|
||||
// MARK: - 主要积分显示区域
|
||||
private var mainCreditsSection: some View {
|
||||
HStack(spacing: Theme.Spacing.md) {
|
||||
HStack(spacing: Theme.Spacing.sm) {
|
||||
// 积分图标和数量
|
||||
HStack(spacing: Theme.Spacing.sm) {
|
||||
Text("Credits:")
|
||||
|
||||
@ -25,7 +25,7 @@ struct AboutUsView: View {
|
||||
VStack(spacing: 0) {
|
||||
// IP Address Section
|
||||
VStack(spacing: 12) {
|
||||
SVGImage(svgName: "AboutIP")
|
||||
SVGImageHtml(svgName: "AboutIP")
|
||||
.frame(width: 102, height: 102)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
@ -33,11 +33,11 @@ struct AboutUsView: View {
|
||||
|
||||
// Version & ICP Info
|
||||
VStack(spacing: 12) {
|
||||
Text("Version : 1.1.1")
|
||||
Text("Version : 2.0.0")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.themeTextMessageMain)
|
||||
|
||||
Text("ICP 备案号: 京ICP备XXXXXXXX号")
|
||||
Text("ICP 备案号: 沪ICP备2025133004号-2A")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.themeTextMessageMain)
|
||||
}
|
||||
|
||||
@ -121,7 +121,7 @@ struct AccountView: View {
|
||||
|
||||
}) {
|
||||
Text("Confirm")
|
||||
.foregroundColor(.themeTextWhite)
|
||||
.foregroundColor(.themeTextMessage)
|
||||
.font(.system(size: 12))
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
|
||||
483
wake/View/Owner/BlindListView.swift
Normal file
483
wake/View/Owner/BlindListView.swift
Normal file
@ -0,0 +1,483 @@
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
import WaterfallGrid
|
||||
|
||||
// MARK: - API Response Models for BlindList
|
||||
struct BlindListMaterialResponse: Decodable {
|
||||
let code: Int
|
||||
let data: [BlindBoxItem]
|
||||
}
|
||||
|
||||
struct BlindBoxItem: Identifiable, Decodable {
|
||||
let id: Int64
|
||||
let boxCode: String
|
||||
let userId: Int64
|
||||
let name: String
|
||||
let boxType: String
|
||||
let features: String?
|
||||
let resultFile: FileInfo?
|
||||
let status: String
|
||||
let workflowInstanceId: Int64?
|
||||
let videoGenerateTime: String?
|
||||
let createTime: String
|
||||
let coverFile: FileInfo?
|
||||
let description: String
|
||||
|
||||
struct FileInfo: Decodable {
|
||||
let id: String
|
||||
let fileName: String
|
||||
let url: String
|
||||
let metadata: [String: String]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case fileName = "file_name"
|
||||
case url
|
||||
case metadata
|
||||
}
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case boxCode = "box_code"
|
||||
case userId = "user_id"
|
||||
case name
|
||||
case boxType = "box_type"
|
||||
case features
|
||||
case resultFile = "result_file"
|
||||
case status
|
||||
case workflowInstanceId = "workflow_instance_id"
|
||||
case videoGenerateTime = "video_generate_time"
|
||||
case createTime = "create_time"
|
||||
case coverFile = "cover_file"
|
||||
case description
|
||||
}
|
||||
}
|
||||
|
||||
enum BlindListMemoryMediaType: Equatable {
|
||||
case image(String)
|
||||
case video(url: String, previewUrl: String)
|
||||
|
||||
var isVideo: Bool {
|
||||
if case .video = self { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
var url: String {
|
||||
switch self {
|
||||
case .image(let url):
|
||||
return url
|
||||
case .video(let url, _):
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - View Models
|
||||
struct BlindListFileInfo {
|
||||
let id: String
|
||||
let fileName: String
|
||||
let url: String
|
||||
}
|
||||
|
||||
struct BlindListMemoryItem: Identifiable {
|
||||
let id: String
|
||||
let name: String
|
||||
let description: String
|
||||
let fileInfo: BlindListFileInfo
|
||||
let previewFileInfo: BlindListFileInfo
|
||||
|
||||
var title: String { name }
|
||||
var subtitle: String { description }
|
||||
|
||||
var mediaType: BlindListMemoryMediaType {
|
||||
// Determine media type based on file extension or other criteria
|
||||
// For now, default to image
|
||||
return .image(fileInfo.url)
|
||||
}
|
||||
|
||||
var aspectRatio: CGFloat { 1.0 }
|
||||
}
|
||||
|
||||
struct BlindListView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var memories: [BlindListMemoryItem] = []
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
@State private var selectedMemory: BlindListMemoryItem? = nil
|
||||
|
||||
let columns = [
|
||||
GridItem(.flexible(), spacing: 1),
|
||||
GridItem(.flexible(), spacing: 1)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
VStack(spacing: 0) {
|
||||
// 顶部导航栏
|
||||
HStack {
|
||||
Button(action: {
|
||||
self.dismiss()
|
||||
}) {
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundColor(.themeTextMessageMain)
|
||||
.font(.system(size: 20))
|
||||
}
|
||||
Spacer()
|
||||
Text("我的盲盒")
|
||||
.foregroundColor(.themeTextMessageMain)
|
||||
.font(Typography.font(for: .body, family: .quicksandBold))
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.background(Color.themeTextWhiteSecondary)
|
||||
|
||||
// 内容区域
|
||||
ZStack {
|
||||
Color.themeTextWhiteSecondary.ignoresSafeArea()
|
||||
|
||||
ScrollView {
|
||||
WaterfallGrid(memories) { memory in
|
||||
BlindListMemoryCard(memory: memory)
|
||||
.onTapGesture {
|
||||
withAnimation(.spring()) {
|
||||
selectedMemory = memory
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 全屏模态
|
||||
if let memory = selectedMemory {
|
||||
BlindListFullScreenMediaView(memory: memory, isPresented: $selectedMemory)
|
||||
.transition(.opacity)
|
||||
.zIndex(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.onAppear {
|
||||
fetchList()
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchList() {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
NetworkService.shared.get(path: "/blind_boxs/query", parameters: nil) { [self] (result: Result<BlindListMaterialResponse, NetworkError>) in
|
||||
DispatchQueue.main.async { [self] in
|
||||
self.isLoading = false
|
||||
|
||||
switch result {
|
||||
case .success(let response):
|
||||
print("✅ Successfully fetched \(response.data.count) blind box items")
|
||||
// Convert BlindBoxItem to BlindListMemoryItem
|
||||
self.memories = response.data
|
||||
.filter { $0.status == "Opened" }
|
||||
.map { item in
|
||||
BlindListMemoryItem(
|
||||
id: String(item.id),
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
fileInfo: BlindListFileInfo(
|
||||
id: item.resultFile?.id ?? "",
|
||||
fileName: item.resultFile?.fileName ?? "",
|
||||
url: item.resultFile?.url ?? ""
|
||||
),
|
||||
previewFileInfo: BlindListFileInfo(
|
||||
id: item.coverFile?.id ?? "",
|
||||
fileName: item.coverFile?.fileName ?? "",
|
||||
url: item.coverFile?.url ?? ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
self.errorMessage = error.localizedDescription
|
||||
print("❌ Failed to fetch blind box items: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BlindListFullScreenMediaView: View {
|
||||
let memory: BlindListMemoryItem
|
||||
@Binding var isPresented: BlindListMemoryItem?
|
||||
@State private var isVideoPlaying = false
|
||||
@State private var showControls = true
|
||||
@State private var controlsTimer: Timer? = nil
|
||||
@State private var imageAspectRatio: CGFloat = 1.0
|
||||
@State private var isLoading = true
|
||||
|
||||
private func loadAspectRatio(from url: URL) {
|
||||
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
|
||||
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
|
||||
let width = imageProperties[kCGImagePropertyPixelWidth] as? CGFloat,
|
||||
let height = imageProperties[kCGImagePropertyPixelHeight] as? CGFloat,
|
||||
height > 0 else {
|
||||
imageAspectRatio = 16/9
|
||||
isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
imageAspectRatio = width / height
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.black.ignoresSafeArea()
|
||||
|
||||
ZStack {
|
||||
GeometryReader { geometry in
|
||||
switch memory.mediaType {
|
||||
case .image(let url):
|
||||
if let imageURL = URL(string: url) {
|
||||
AsyncImage(url: imageURL) { phase in
|
||||
switch phase {
|
||||
case .success(let image):
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
Color.black
|
||||
image
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(
|
||||
width: min(geometry.size.width, geometry.size.height * imageAspectRatio),
|
||||
height: min(geometry.size.height, geometry.size.width / imageAspectRatio)
|
||||
)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
.onAppear {
|
||||
if let uiImage = image.asUIImage() {
|
||||
let size = uiImage.size
|
||||
imageAspectRatio = size.width / size.height
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
case .failure(_):
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.foregroundColor(.red)
|
||||
case .empty:
|
||||
ProgressView()
|
||||
@unknown default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case .video(_, let previewUrl):
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
Color.clear
|
||||
BlindListVideoPlayer(url: memory.mediaType.url, isPlaying: $isVideoPlaying)
|
||||
.aspectRatio(imageAspectRatio, contentMode: .fit)
|
||||
.frame(
|
||||
width: min(geometry.size.width, geometry.size.height * imageAspectRatio),
|
||||
height: min(geometry.size.height, geometry.size.width / imageAspectRatio)
|
||||
)
|
||||
.onAppear {
|
||||
if let previewUrl = URL(string: previewUrl) {
|
||||
loadAspectRatio(from: previewUrl)
|
||||
}
|
||||
isVideoPlaying = true
|
||||
}
|
||||
.onDisappear {
|
||||
isVideoPlaying = false
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Button(action: {
|
||||
withAnimation(.spring()) {
|
||||
isPresented = nil
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 20, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
.padding(12)
|
||||
.background(Circle().fill(Color.black.opacity(0.4)))
|
||||
}
|
||||
.padding(.leading, 16)
|
||||
.padding(.top, UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.zIndex(2)
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.statusBar(hidden: true)
|
||||
}
|
||||
.onTapGesture {
|
||||
if case .video = memory.mediaType {
|
||||
withAnimation(.easeInOut) {
|
||||
showControls.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
.statusBar(hidden: true)
|
||||
.onAppear {
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
}
|
||||
.onDisappear {
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
controlsTimer?.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BlindListVideoPlayer: UIViewControllerRepresentable {
|
||||
let url: String
|
||||
@Binding var isPlaying: Bool
|
||||
|
||||
func makeUIViewController(context: Context) -> AVPlayerViewController {
|
||||
let controller = AVPlayerViewController()
|
||||
let player = AVPlayer(url: URL(string: url)!)
|
||||
controller.player = player
|
||||
controller.showsPlaybackControls = true
|
||||
controller.videoGravity = .resizeAspect
|
||||
controller.view.backgroundColor = .clear
|
||||
controller.view.isOpaque = false
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
|
||||
if isPlaying {
|
||||
uiViewController.player?.play()
|
||||
} else {
|
||||
uiViewController.player?.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BlindListMemoryCard: View {
|
||||
let memory: BlindListMemoryItem
|
||||
@State private var aspectRatio: CGFloat = 1.0
|
||||
@State private var isLoading = true
|
||||
|
||||
private func loadAspectRatio(from url: URL) {
|
||||
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
|
||||
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
|
||||
let width = imageProperties[kCGImagePropertyPixelWidth] as? CGFloat,
|
||||
let height = imageProperties[kCGImagePropertyPixelHeight] as? CGFloat,
|
||||
height > 0 else {
|
||||
aspectRatio = 16/9
|
||||
isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
aspectRatio = width / height
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ZStack {
|
||||
Group {
|
||||
switch memory.mediaType {
|
||||
case .image(let url):
|
||||
if let url = URL(string: url) {
|
||||
AsyncImage(url: url) { phase in
|
||||
Group {
|
||||
if let image = phase.image {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
Color.black
|
||||
image
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(
|
||||
width: min(geometry.size.width, geometry.size.height * aspectRatio),
|
||||
height: min(geometry.size.height, geometry.size.width / aspectRatio)
|
||||
)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
.aspectRatio(aspectRatio, contentMode: aspectRatio > 1 ? .fit : .fill)
|
||||
.onAppear {
|
||||
if let uiImage = image.asUIImage() {
|
||||
let size = uiImage.size
|
||||
aspectRatio = size.width / size.height
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
} else if phase.error != nil {
|
||||
Color.gray.opacity(0.3)
|
||||
} else {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case .video(_, let previewUrl):
|
||||
if let previewUrl = URL(string: previewUrl) {
|
||||
AsyncImage(url: previewUrl) { phase in
|
||||
Group {
|
||||
if let image = phase.image {
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.onAppear {
|
||||
loadAspectRatio(from: previewUrl)
|
||||
}
|
||||
} else if phase.error != nil {
|
||||
Color.gray.opacity(0.3)
|
||||
} else {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Color.gray.opacity(0.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(
|
||||
width: (UIScreen.main.bounds.width / 2) - 24,
|
||||
height: (UIScreen.main.bounds.width / 2 - 24) / (isLoading ? 1 : aspectRatio)
|
||||
)
|
||||
.clipped()
|
||||
.cornerRadius(12)
|
||||
|
||||
if case .video = memory.mediaType {
|
||||
Image(systemName: "play.circle.fill")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.shadow(radius: 3)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(memory.title)
|
||||
.font(Typography.font(for: .body, family: .quicksandBold))
|
||||
.foregroundColor(.themeTextMessageMain)
|
||||
.lineLimit(1)
|
||||
|
||||
Text(memory.subtitle)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.themeTextMessageMain)
|
||||
.lineLimit(2)
|
||||
}
|
||||
.padding(.horizontal, 2)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -26,18 +26,24 @@ struct PermissionManagementView: View {
|
||||
// 1. 相册权限
|
||||
PermissionRow(
|
||||
title: "Gallery Permissions",
|
||||
isEnabled: photoLibraryStatus == .authorized
|
||||
) {
|
||||
requestPhotoLibraryPermission() // 请求相册权限
|
||||
}
|
||||
isEnabled: photoLibraryStatus == .authorized,
|
||||
action: {
|
||||
requestPhotoLibraryPermission()
|
||||
},
|
||||
openSettings: openAppSettings
|
||||
)
|
||||
.background(Color.white)
|
||||
|
||||
// 2. 通知权限
|
||||
PermissionRow(
|
||||
title: "Notification Permissions",
|
||||
isEnabled: notificationStatus == .authorized
|
||||
) {
|
||||
requestNotificationPermission() // 请求通知权限
|
||||
}
|
||||
isEnabled: notificationStatus == .authorized,
|
||||
action: {
|
||||
requestNotificationPermission()
|
||||
},
|
||||
openSettings: openAppSettings
|
||||
)
|
||||
.background(Color.white)
|
||||
}
|
||||
.background(Color.white)
|
||||
.cornerRadius(16)
|
||||
@ -120,9 +126,11 @@ struct PermissionManagementView: View {
|
||||
|
||||
/// 打开应用设置页面
|
||||
private func openAppSettings() {
|
||||
if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||
UIApplication.shared.open(url)
|
||||
guard let settingsUrl = URL(string: UIApplication.openSettingsURLString),
|
||||
UIApplication.shared.canOpenURL(settingsUrl) else {
|
||||
return
|
||||
}
|
||||
UIApplication.shared.open(settingsUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,6 +139,14 @@ struct PermissionRow: View {
|
||||
let title: String // 权限名称
|
||||
let isEnabled: Bool // 是否已授权
|
||||
let action: () -> Void // 点击事件
|
||||
let openSettings: () -> Void // 打开设置页面的函数
|
||||
|
||||
init(title: String, isEnabled: Bool, action: @escaping () -> Void, openSettings: @escaping () -> Void) {
|
||||
self.title = title
|
||||
self.isEnabled = isEnabled
|
||||
self.action = action
|
||||
self.openSettings = openSettings
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
@ -143,29 +159,27 @@ struct PermissionRow: View {
|
||||
Spacer()
|
||||
|
||||
// 权限开关
|
||||
Toggle("", isOn: .constant(isEnabled))
|
||||
.labelsHidden()
|
||||
.tint(Color.themePrimary)
|
||||
.disabled(true)
|
||||
.onAppear {
|
||||
// 使用主题色并确保完全不透明
|
||||
let themeColor = UIColor(Color.themePrimary)
|
||||
UISwitch.appearance(whenContainedInInstancesOf: [UIView.self]).onTintColor = themeColor
|
||||
UISwitch.appearance(whenContainedInInstancesOf: [UIView.self]).thumbTintColor = .white
|
||||
// 确保使用不透明的背景
|
||||
UISwitch.appearance(whenContainedInInstancesOf: [UIView.self]).backgroundColor = UIColor.clear
|
||||
Toggle("", isOn: .init(
|
||||
get: { isEnabled },
|
||||
set: { newValue in
|
||||
// 打开系统设置
|
||||
openSettings()
|
||||
}
|
||||
))
|
||||
.labelsHidden()
|
||||
.tint(Color.themePrimary)
|
||||
.onAppear {
|
||||
// 使用主题色并确保完全不透明
|
||||
let themeColor = UIColor(Color.themePrimary)
|
||||
UISwitch.appearance(whenContainedInInstancesOf: [UIView.self]).onTintColor = themeColor
|
||||
UISwitch.appearance(whenContainedInInstancesOf: [UIView.self]).thumbTintColor = .white
|
||||
// 确保使用不透明的背景
|
||||
UISwitch.appearance(whenContainedInInstancesOf: [UIView.self]).backgroundColor = UIColor.clear
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
// 添加底部边框
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundColor(Color(.systemGray6)),
|
||||
alignment: .bottom
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -60,7 +60,11 @@ struct SettingsView: View {
|
||||
settingRow(
|
||||
icon: "Suport",
|
||||
title: "Support & Service",
|
||||
action: {}
|
||||
action: {
|
||||
if let url = URL(string: "https://work.weixin.qq.com/kfid/kfca0ac87f4e05e8bfd") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 关于我们
|
||||
|
||||
@ -116,7 +116,7 @@ struct UserInfo: View {
|
||||
// Content VStack
|
||||
VStack(spacing: 20) {
|
||||
// Title
|
||||
Text(showUsername ? "Add Your Avatar" : "What's Your Name?")
|
||||
Text(showUsername ? "What's Your Name?" : "Add Your Avatar")
|
||||
.font(Typography.font(for: .body, family: .quicksandBold))
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
|
||||
@ -97,6 +97,9 @@ struct SubscriptionStatusBar: View {
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||
}
|
||||
.frame(height: height)
|
||||
.onTapGesture {
|
||||
onSubscribeTap?()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 日期格式化
|
||||
|
||||
@ -94,7 +94,7 @@ struct SubscribeView: View {
|
||||
// 法律链接
|
||||
legalLinks
|
||||
|
||||
Spacer(minLength: 100)
|
||||
Spacer(minLength: 10)
|
||||
}
|
||||
}
|
||||
.background(Theme.Colors.background)
|
||||
|
||||
@ -328,7 +328,10 @@ struct MediaUploadView: View {
|
||||
"preview_file_id": result.thumbnailId ?? result.fileId
|
||||
]
|
||||
}
|
||||
|
||||
let filesID = uploadResults.map { (_, result) -> String in
|
||||
return result.fileId
|
||||
}
|
||||
|
||||
// 发送POST请求到/material接口
|
||||
NetworkService.shared.postWithToken(
|
||||
path: "/material",
|
||||
@ -337,9 +340,9 @@ struct MediaUploadView: View {
|
||||
switch result {
|
||||
case .success:
|
||||
print("✅ 素材提交成功")
|
||||
// 跳转到盲盒页面
|
||||
// 跳转到盲盒页面,传递上传的文件信息
|
||||
DispatchQueue.main.async {
|
||||
Router.shared.navigate(to: .blindBox(mediaType: .video))
|
||||
Router.shared.navigate(to: .blindBox(mediaType: .video, ids: filesID))
|
||||
}
|
||||
case .failure(let error):
|
||||
print("❌ 素材提交失败: \(error.localizedDescription)")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user