feat: 暂提
This commit is contained in:
parent
7e6f4b5359
commit
02cd217053
24
wake/Assets/Svg/Free.svg
Normal file
24
wake/Assets/Svg/Free.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 17 KiB |
@ -105,36 +105,6 @@ struct BlindBoxView: View {
|
|||||||
case description
|
case description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 会员信息
|
|
||||||
struct MemberProfile: Codable {
|
|
||||||
let materialCounter: MaterialCounter
|
|
||||||
let userInfo: UserInfo
|
|
||||||
let storiesCount: Int
|
|
||||||
let conversationsCount: Int
|
|
||||||
let remainPoints: Int
|
|
||||||
let totalPoints: Int
|
|
||||||
let usedBytes: Int
|
|
||||||
let totalBytes: Int
|
|
||||||
let titleRankings: [String]
|
|
||||||
let medalInfos: [MedalInfo]
|
|
||||||
let membershipLevel: String
|
|
||||||
let membershipEndAt: String
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case materialCounter = "material_counter"
|
|
||||||
case userInfo = "user_info"
|
|
||||||
case storiesCount = "stories_count"
|
|
||||||
case conversationsCount = "conversations_count"
|
|
||||||
case remainPoints = "remain_points"
|
|
||||||
case totalPoints = "total_points"
|
|
||||||
case usedBytes = "used_bytes"
|
|
||||||
case totalBytes = "total_bytes"
|
|
||||||
case titleRankings = "title_rankings"
|
|
||||||
case medalInfos = "medal_infos"
|
|
||||||
case membershipLevel = "membership_level"
|
|
||||||
case membershipEndAt = "membership_end_at"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 盲盒数量
|
// 盲盒数量
|
||||||
struct BlindCount: Codable {
|
struct BlindCount: Codable {
|
||||||
let availableQuantity: Int
|
let availableQuantity: Int
|
||||||
@ -144,59 +114,6 @@ struct BlindBoxView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MaterialCounter: Codable {
|
|
||||||
let userId: Int64
|
|
||||||
let totalCount: MediaCount
|
|
||||||
let categoryCount: [String: MediaCount]
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case userId = "user_id"
|
|
||||||
case totalCount = "total_count"
|
|
||||||
case categoryCount = "category_count"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MediaCount: Codable {
|
|
||||||
let videoCount: Int
|
|
||||||
let photoCount: Int
|
|
||||||
let liveCount: Int
|
|
||||||
let videoLength: Double
|
|
||||||
let coverUrl: String?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case videoCount = "video_count"
|
|
||||||
case photoCount = "photo_count"
|
|
||||||
case liveCount = "live_count"
|
|
||||||
case videoLength = "video_length"
|
|
||||||
case coverUrl = "cover_url"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UserInfo: Codable {
|
|
||||||
let userId: String
|
|
||||||
let accessToken: String
|
|
||||||
let avatarFileUrl: String?
|
|
||||||
let nickname: String
|
|
||||||
let account: String
|
|
||||||
let email: String
|
|
||||||
let refreshToken: String?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case userId = "user_id"
|
|
||||||
case accessToken = "access_token"
|
|
||||||
case avatarFileUrl = "avatar_file_url"
|
|
||||||
case nickname
|
|
||||||
case account
|
|
||||||
case email
|
|
||||||
case refreshToken = "refresh_token"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MedalInfo: Codable, Identifiable {
|
|
||||||
let id: Int
|
|
||||||
let url: String
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - BlindBox Response Model
|
// MARK: - BlindBox Response Model
|
||||||
|
|
||||||
struct BlindBoxData: Codable {
|
struct BlindBoxData: Codable {
|
||||||
@ -353,15 +270,13 @@ struct BlindBoxView: View {
|
|||||||
NetworkService.shared.get(
|
NetworkService.shared.get(
|
||||||
path: "/membership/personal-center-info",
|
path: "/membership/personal-center-info",
|
||||||
parameters: nil
|
parameters: nil
|
||||||
) { (result: Result<APIResponse<MemberProfile>, NetworkError>) in
|
) { (result: Result<MemberProfileResponse, NetworkError>) in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let response):
|
case .success(let response):
|
||||||
self.memberProfile = response.data
|
self.memberProfile = response.data
|
||||||
print("✅ 成功获取会员信息:", response.data)
|
print("✅ 成功获取会员信息:", response.data)
|
||||||
print("✅ 用户ID:", response.data.userInfo.userId)
|
print("✅ 用户ID:", response.data.userInfo.userId)
|
||||||
print("✅ 用户昵称:", response.data.userInfo.nickname)
|
|
||||||
print("✅ 用户邮箱:", response.data.userInfo.email)
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
print("❌ 获取会员信息失败:", error)
|
print("❌ 获取会员信息失败:", error)
|
||||||
}
|
}
|
||||||
@ -472,24 +387,6 @@ struct BlindBoxView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadData() {
|
|
||||||
// 会员信息
|
|
||||||
NetworkService.shared.get(
|
|
||||||
path: "/membership/personal-center-info",
|
|
||||||
parameters: nil
|
|
||||||
) { (result: Result<APIResponse<MemberProfile>, NetworkError>) in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
switch result {
|
|
||||||
case .success(let response):
|
|
||||||
self.memberProfile = response.data
|
|
||||||
print("✅ Successfully fetched user info:", response.data)
|
|
||||||
case .failure(let error):
|
|
||||||
print("❌ Failed to fetch user info:", error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadImage() {
|
private func loadImage() {
|
||||||
guard !imageURL.isEmpty, let url = URL(string: imageURL) else {
|
guard !imageURL.isEmpty, let url = URL(string: imageURL) else {
|
||||||
print("⚠️ 图片URL无效或为空")
|
print("⚠️ 图片URL无效或为空")
|
||||||
|
|||||||
255
wake/Models/MemberProfile.swift
Normal file
255
wake/Models/MemberProfile.swift
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - MemberProfile Response
|
||||||
|
struct MemberProfileResponse: Codable {
|
||||||
|
let code: Int
|
||||||
|
let data: MemberProfile
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case code, data
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(code, forKey: .code)
|
||||||
|
try container.encode(data, forKey: .data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - MemberProfile
|
||||||
|
struct MemberProfile: Codable {
|
||||||
|
let materialCounter: MaterialCounter
|
||||||
|
let userInfo: MemberUserInfo
|
||||||
|
let storiesCount: Int
|
||||||
|
let conversationsCount: Int
|
||||||
|
let remainPoints: Int
|
||||||
|
let totalPoints: Int
|
||||||
|
let usedBytes: Int
|
||||||
|
let totalBytes: Int
|
||||||
|
let titleRankings: [String]
|
||||||
|
let medalInfos: [MedalInfo]
|
||||||
|
let membershipLevel: String
|
||||||
|
let membershipEndAt: String
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case materialCounter = "material_counter"
|
||||||
|
case userInfo = "user_info"
|
||||||
|
case storiesCount = "stories_count"
|
||||||
|
case conversationsCount = "conversations_count"
|
||||||
|
case remainPoints = "remain_points"
|
||||||
|
case totalPoints = "total_points"
|
||||||
|
case usedBytes = "used_bytes"
|
||||||
|
case totalBytes = "total_bytes"
|
||||||
|
case titleRankings = "title_rankings"
|
||||||
|
case medalInfos = "medal_infos"
|
||||||
|
case membershipLevel = "membership_level"
|
||||||
|
case membershipEndAt = "membership_end_at"
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
|
materialCounter = try container.decode(MaterialCounter.self, forKey: .materialCounter)
|
||||||
|
userInfo = try container.decode(MemberUserInfo.self, forKey: .userInfo)
|
||||||
|
storiesCount = try container.decode(Int.self, forKey: .storiesCount)
|
||||||
|
conversationsCount = try container.decode(Int.self, forKey: .conversationsCount)
|
||||||
|
remainPoints = try container.decode(Int.self, forKey: .remainPoints)
|
||||||
|
totalPoints = try container.decode(Int.self, forKey: .totalPoints)
|
||||||
|
usedBytes = try container.decode(Int.self, forKey: .usedBytes)
|
||||||
|
totalBytes = try container.decode(Int.self, forKey: .totalBytes)
|
||||||
|
titleRankings = try container.decode([String].self, forKey: .titleRankings)
|
||||||
|
|
||||||
|
if let medalInfos = try? container.decode([MedalInfo].self, forKey: .medalInfos) {
|
||||||
|
self.medalInfos = medalInfos
|
||||||
|
} else {
|
||||||
|
self.medalInfos = []
|
||||||
|
}
|
||||||
|
|
||||||
|
membershipLevel = try container.decode(String.self, forKey: .membershipLevel)
|
||||||
|
membershipEndAt = try container.decode(String.self, forKey: .membershipEndAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(materialCounter, forKey: .materialCounter)
|
||||||
|
try container.encode(userInfo, forKey: .userInfo)
|
||||||
|
try container.encode(storiesCount, forKey: .storiesCount)
|
||||||
|
try container.encode(conversationsCount, forKey: .conversationsCount)
|
||||||
|
try container.encode(remainPoints, forKey: .remainPoints)
|
||||||
|
try container.encode(totalPoints, forKey: .totalPoints)
|
||||||
|
try container.encode(usedBytes, forKey: .usedBytes)
|
||||||
|
try container.encode(totalBytes, forKey: .totalBytes)
|
||||||
|
try container.encode(titleRankings, forKey: .titleRankings)
|
||||||
|
try container.encode(medalInfos, forKey: .medalInfos)
|
||||||
|
try container.encode(membershipLevel, forKey: .membershipLevel)
|
||||||
|
try container.encode(membershipEndAt, forKey: .membershipEndAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - MemberUserInfo
|
||||||
|
struct MemberUserInfo: Codable {
|
||||||
|
let userId: String
|
||||||
|
let accessToken: String
|
||||||
|
let avatarFileUrl: String?
|
||||||
|
let nickname: String
|
||||||
|
let account: String
|
||||||
|
let email: String
|
||||||
|
let refreshToken: String?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case userId = "user_id"
|
||||||
|
case accessToken = "access_token"
|
||||||
|
case avatarFileUrl = "avatar_file_url"
|
||||||
|
case nickname, account, email
|
||||||
|
case refreshToken = "refresh_token"
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(userId, forKey: .userId)
|
||||||
|
try container.encode(accessToken, forKey: .accessToken)
|
||||||
|
try container.encodeIfPresent(avatarFileUrl, forKey: .avatarFileUrl)
|
||||||
|
try container.encode(nickname, forKey: .nickname)
|
||||||
|
try container.encode(account, forKey: .account)
|
||||||
|
try container.encode(email, forKey: .email)
|
||||||
|
try container.encodeIfPresent(refreshToken, forKey: .refreshToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - MaterialCounter
|
||||||
|
struct MaterialCounter: Codable {
|
||||||
|
let userId: Int64
|
||||||
|
let totalCount: TotalCount
|
||||||
|
let categoryCount: [String: CategoryCount]
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case userId = "user_id"
|
||||||
|
case totalCount = "total_count"
|
||||||
|
case categoryCount = "category_count"
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(userId, forKey: .userId)
|
||||||
|
try container.encode(totalCount, forKey: .totalCount)
|
||||||
|
try container.encode(categoryCount, forKey: .categoryCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TotalCount
|
||||||
|
struct TotalCount: Codable {
|
||||||
|
let videoCount: Int
|
||||||
|
let photoCount: Int
|
||||||
|
let liveCount: Int
|
||||||
|
let videoLength: Double
|
||||||
|
let coverUrl: String?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case videoCount = "video_count"
|
||||||
|
case photoCount = "photo_count"
|
||||||
|
case liveCount = "live_count"
|
||||||
|
case videoLength = "video_length"
|
||||||
|
case coverUrl = "cover_url"
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(videoCount, forKey: .videoCount)
|
||||||
|
try container.encode(photoCount, forKey: .photoCount)
|
||||||
|
try container.encode(liveCount, forKey: .liveCount)
|
||||||
|
try container.encode(videoLength, forKey: .videoLength)
|
||||||
|
try container.encodeIfPresent(coverUrl, forKey: .coverUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - CategoryCount
|
||||||
|
struct CategoryCount: Codable {
|
||||||
|
let videoCount: Int
|
||||||
|
let photoCount: Int
|
||||||
|
let liveCount: Int
|
||||||
|
let videoLength: Double
|
||||||
|
let coverUrl: String?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case videoCount = "video_count"
|
||||||
|
case photoCount = "photo_count"
|
||||||
|
case liveCount = "live_count"
|
||||||
|
case videoLength = "video_length"
|
||||||
|
case coverUrl = "cover_url"
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(videoCount, forKey: .videoCount)
|
||||||
|
try container.encode(photoCount, forKey: .photoCount)
|
||||||
|
try container.encode(liveCount, forKey: .liveCount)
|
||||||
|
try container.encode(videoLength, forKey: .videoLength)
|
||||||
|
try container.encodeIfPresent(coverUrl, forKey: .coverUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - MedalInfo
|
||||||
|
struct MedalInfo: Codable, Identifiable {
|
||||||
|
let id: Int
|
||||||
|
let url: String
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id, url
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(id, forKey: .id)
|
||||||
|
try container.encode(url, forKey: .url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - API Response Wrapper
|
||||||
|
struct MemberAPIResponse<T: Codable>: Codable {
|
||||||
|
let code: Int
|
||||||
|
let message: String
|
||||||
|
let data: T
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case code, message, data
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(code, forKey: .code)
|
||||||
|
try container.encode(message, forKey: .message)
|
||||||
|
try container.encode(data, forKey: .data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Date Formatter
|
||||||
|
class DateFormatterManager {
|
||||||
|
static let shared = DateFormatterManager()
|
||||||
|
|
||||||
|
let iso8601Full: ISO8601DateFormatter = {
|
||||||
|
let formatter = ISO8601DateFormatter()
|
||||||
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - JSON Decoder Extension
|
||||||
|
extension JSONDecoder {
|
||||||
|
static let `default`: JSONDecoder = {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
decoder.dateDecodingStrategy = .custom { decoder -> Date in
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
let dateString = try container.decode(String.self)
|
||||||
|
|
||||||
|
if let date = DateFormatterManager.shared.iso8601Full.date(from: dateString) {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
|
||||||
|
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateString)")
|
||||||
|
}
|
||||||
|
return decoder
|
||||||
|
}()
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ import WebKit
|
|||||||
|
|
||||||
struct SVGImage: UIViewRepresentable {
|
struct SVGImage: UIViewRepresentable {
|
||||||
let svgName: String
|
let svgName: String
|
||||||
|
var shouldFill: Bool = false
|
||||||
|
|
||||||
func makeUIView(context: Context) -> WKWebView {
|
func makeUIView(context: Context) -> WKWebView {
|
||||||
let webView = WKWebView()
|
let webView = WKWebView()
|
||||||
@ -11,20 +12,23 @@ struct SVGImage: UIViewRepresentable {
|
|||||||
webView.scrollView.isScrollEnabled = false
|
webView.scrollView.isScrollEnabled = false
|
||||||
webView.scrollView.contentInsetAdjustmentBehavior = .never
|
webView.scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
|
|
||||||
// 1. 获取 SVG 文件路径(注意:移除了 inDirectory 参数)
|
|
||||||
guard let path = Bundle.main.path(forResource: svgName, ofType: "svg") else {
|
guard let path = Bundle.main.path(forResource: svgName, ofType: "svg") else {
|
||||||
print("❌ 无法找到 SVG 文件: \(svgName).svg")
|
print("❌ 无法找到 SVG 文件: \(svgName).svg")
|
||||||
// 打印所有可用的资源文件,用于调试
|
|
||||||
if let resourcePath = Bundle.main.resourcePath {
|
|
||||||
print("可用的资源文件: \(try? FileManager.default.contentsOfDirectory(atPath: resourcePath))")
|
|
||||||
}
|
|
||||||
return webView
|
return webView
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 创建文件 URL
|
|
||||||
let fileURL = URL(fileURLWithPath: path)
|
let fileURL = URL(fileURLWithPath: path)
|
||||||
|
|
||||||
// 3. 创建 HTML 字符串
|
let svgStyle = shouldFill ? """
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
""" : """
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
"""
|
||||||
|
|
||||||
let htmlString = """
|
let htmlString = """
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
@ -39,30 +43,50 @@ struct SVGImage: UIViewRepresentable {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
svg {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: block;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
img {
|
svg {
|
||||||
max-width: 100%;
|
\(svgStyle)
|
||||||
max-height: 100%;
|
display: block;
|
||||||
object-fit: contain;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div style="width:100%; height:100%; display:flex; align-items:center; justify-content:center;">
|
<div class="container">
|
||||||
<img src="\(fileURL.lastPathComponent)" />
|
<object type="image/svg+xml" data="\(fileURL.lastPathComponent)"></object>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
// 4. 加载 HTML 字符串
|
|
||||||
webView.loadHTMLString(htmlString, baseURL: fileURL.deletingLastPathComponent())
|
webView.loadHTMLString(htmlString, baseURL: fileURL.deletingLastPathComponent())
|
||||||
return webView
|
return webView
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ uiView: WKWebView, context: Context) {}
|
func updateUIView(_ uiView: WKWebView, context: Context) {}
|
||||||
|
|
||||||
|
func sizeThatFits(_ proposal: ProposedViewSize, uiView: WKWebView, context: Context) -> CGSize? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Preview
|
||||||
|
#Preview {
|
||||||
|
VStack {
|
||||||
|
Text("Filled SVG")
|
||||||
|
SVGImage(svgName: "YourSVGName", shouldFill: true)
|
||||||
|
.frame(width: 200, height: 100)
|
||||||
|
.background(Color.gray.opacity(0.2))
|
||||||
|
|
||||||
|
Text("Intrinsic Size SVG")
|
||||||
|
SVGImage(svgName: "YourSVGName", shouldFill: false)
|
||||||
|
.frame(width: 200, height: 100)
|
||||||
|
.background(Color.gray.opacity(0.2))
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
}
|
}
|
||||||
@ -15,27 +15,18 @@ enum SubscriptionStatus {
|
|||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .free:
|
case .free:
|
||||||
return "Free"
|
return ""
|
||||||
case .pioneer:
|
case .pioneer:
|
||||||
return "Pioneer"
|
return "Pioneer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasExpiry: Bool {
|
|
||||||
switch self {
|
|
||||||
case .free:
|
|
||||||
return false
|
|
||||||
case .pioneer:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var backgroundColor: Color {
|
var backgroundColor: Color {
|
||||||
switch self {
|
switch self {
|
||||||
case .free:
|
case .free:
|
||||||
return Theme.Colors.freeBackground // 浅橙色背景
|
return .clear
|
||||||
case .pioneer:
|
case .pioneer:
|
||||||
return Theme.Colors.pioneerBackground // 橙色背景
|
return .clear
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,7 +35,16 @@ enum SubscriptionStatus {
|
|||||||
case .free:
|
case .free:
|
||||||
return Theme.Colors.textPrimary
|
return Theme.Colors.textPrimary
|
||||||
case .pioneer:
|
case .pioneer:
|
||||||
return Theme.Colors.textPrimary
|
return .themeTextMessageMain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var backgroundImageName: String {
|
||||||
|
switch self {
|
||||||
|
case .free:
|
||||||
|
return "Free"
|
||||||
|
case .pioneer:
|
||||||
|
return "Pioneer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,81 +60,73 @@ struct SubscriptionStatusBar: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 16) {
|
ZStack(alignment: .topLeading) {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
// Background SVG - First layer
|
||||||
// 订阅类型标题
|
SVGImage(svgName: status.backgroundImageName, shouldFill: true)
|
||||||
|
.frame(maxWidth: .infinity, minHeight: 120)
|
||||||
|
.clipped()
|
||||||
|
|
||||||
|
// Content - Second layer
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
// Subscription title
|
||||||
Text(status.title)
|
Text(status.title)
|
||||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||||
.foregroundColor(status.textColor)
|
.foregroundColor(status.textColor)
|
||||||
|
.padding(.leading, 24)
|
||||||
|
|
||||||
// 过期时间或订阅按钮
|
// Expiry date or subscribe button
|
||||||
if case .pioneer(let expiryDate) = status {
|
if case .pioneer(let expiryDate) = status {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("Expires on :")
|
Text("Expires on:")
|
||||||
.font(.system(size: 14, weight: .medium))
|
.font(.system(size: 14, weight: .medium))
|
||||||
.foregroundColor(status.textColor.opacity(0.8))
|
.foregroundColor(status.textColor.opacity(0.9))
|
||||||
|
|
||||||
Text(formatDate(expiryDate))
|
Text(formatDate(expiryDate))
|
||||||
.font(.system(size: 16, weight: .semibold))
|
.font(.system(size: 16, weight: .semibold))
|
||||||
.foregroundColor(status.textColor)
|
.foregroundColor(status.textColor)
|
||||||
}
|
}
|
||||||
} else {
|
.padding(.leading, 24)
|
||||||
Button(action: {
|
.padding(.bottom, 20)
|
||||||
onSubscribeTap?()
|
|
||||||
}) {
|
|
||||||
Text("Subscribe")
|
|
||||||
.font(.system(size: 14, weight: .semibold))
|
|
||||||
.foregroundColor(Theme.Colors.textPrimary)
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.background(Theme.Colors.subscribeButton)
|
|
||||||
.cornerRadius(Theme.CornerRadius.large)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
|
||||||
}
|
}
|
||||||
|
.frame(height: 155)
|
||||||
Spacer()
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
// 播放按钮图标
|
|
||||||
Circle()
|
|
||||||
.fill(Color.black)
|
|
||||||
.frame(width: 60, height: 60)
|
|
||||||
.overlay(
|
|
||||||
Image(systemName: "play.fill")
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.font(.title2)
|
|
||||||
.offset(x: 2) // 微调播放图标位置
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding(20)
|
|
||||||
.background(status.backgroundColor)
|
|
||||||
.cornerRadius(20)
|
.cornerRadius(20)
|
||||||
// .shadow(color: Color.black.opacity(0.1), radius: 4, x: 0, y: 2)
|
.clipped()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 日期格式化
|
// MARK: - 日期格式化
|
||||||
private func formatDate(_ date: Date) -> String {
|
private func formatDate(_ date: Date) -> String {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateFormat = "MMMM d, yyyy"
|
formatter.dateFormat = "MMM d, yyyy"
|
||||||
return formatter.string(from: date)
|
return formatter.string(from: date)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 预览
|
// MARK: - 预览
|
||||||
#Preview("Free Status") {
|
#Preview {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
|
// Free status preview
|
||||||
SubscriptionStatusBar(
|
SubscriptionStatusBar(
|
||||||
status: .free,
|
status: .free,
|
||||||
onSubscribeTap: {
|
onSubscribeTap: {
|
||||||
print("Subscribe tapped")
|
print("Subscribe tapped")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.padding()
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
// Pioneer status preview
|
||||||
SubscriptionStatusBar(
|
SubscriptionStatusBar(
|
||||||
status: .pioneer(expiryDate: Calendar.current.date(byAdding: .month, value: 6, to: Date()) ?? Date())
|
status: .pioneer(
|
||||||
|
expiryDate: Calendar.current.date(byAdding: .month, value: 6, to: Date()) ?? Date()
|
||||||
)
|
)
|
||||||
.padding()
|
)
|
||||||
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
.background(Color(.systemGroupedBackground))
|
.padding()
|
||||||
|
.background(Theme.Colors.background)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,7 +45,7 @@ struct SubscribeView: View {
|
|||||||
@State private var isLoading = false
|
@State private var isLoading = false
|
||||||
@State private var showErrorAlert = false
|
@State private var showErrorAlert = false
|
||||||
@State private var errorText = ""
|
@State private var errorText = ""
|
||||||
|
@State private var memberProfile: MemberProfile?
|
||||||
|
|
||||||
// 功能对比数据
|
// 功能对比数据
|
||||||
private let features = [
|
private let features = [
|
||||||
@ -100,6 +100,9 @@ struct SubscribeView: View {
|
|||||||
}
|
}
|
||||||
.background(Color.themeTextWhiteSecondary)
|
.background(Color.themeTextWhiteSecondary)
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
|
.onAppear {
|
||||||
|
fetchMemberInfo()
|
||||||
|
}
|
||||||
.task {
|
.task {
|
||||||
// Load products and refresh current entitlements on appear
|
// Load products and refresh current entitlements on appear
|
||||||
await store.loadProducts()
|
await store.loadProducts()
|
||||||
@ -125,15 +128,18 @@ struct SubscribeView: View {
|
|||||||
// MARK: - 当前订阅状态卡片
|
// MARK: - 当前订阅状态卡片
|
||||||
private var currentSubscriptionCard: some View {
|
private var currentSubscriptionCard: some View {
|
||||||
let status: SubscriptionStatus = {
|
let status: SubscriptionStatus = {
|
||||||
if store.isSubscribed {
|
if memberProfile?.membershipLevel == "pioneer" {
|
||||||
return .pioneer(expiryDate: store.subscriptionExpiry ?? Date())
|
let dateFormatter = ISO8601DateFormatter()
|
||||||
|
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||||
|
let expiryDate = memberProfile.flatMap { dateFormatter.date(from: $0.membershipEndAt) } ?? Date()
|
||||||
|
return .pioneer(expiryDate: expiryDate)
|
||||||
} else {
|
} else {
|
||||||
return .free
|
return .free
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return SubscriptionStatusBar(
|
return SubscriptionStatusBar(
|
||||||
status: status,
|
status: .pioneer(expiryDate: Date()),
|
||||||
onSubscribeTap: {
|
onSubscribeTap: {
|
||||||
// 订阅操作
|
// 订阅操作
|
||||||
handleSubscribe()
|
handleSubscribe()
|
||||||
@ -146,7 +152,7 @@ struct SubscribeView: View {
|
|||||||
private var creditsSection: some View {
|
private var creditsSection: some View {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
CreditsInfoCard(
|
CreditsInfoCard(
|
||||||
totalCredits: 3290,
|
totalCredits: memberProfile?.remainPoints ?? 0,
|
||||||
onInfoTap: {
|
onInfoTap: {
|
||||||
// 显示积分信息说明
|
// 显示积分信息说明
|
||||||
},
|
},
|
||||||
@ -260,6 +266,30 @@ struct SubscribeView: View {
|
|||||||
private func handleSubscribe() {
|
private func handleSubscribe() {
|
||||||
Task { await store.purchasePioneer() }
|
Task { await store.purchasePioneer() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Helper Methods
|
||||||
|
private func fetchMemberInfo() {
|
||||||
|
NetworkService.shared.get(
|
||||||
|
path: "/membership/personal-center-info",
|
||||||
|
parameters: nil
|
||||||
|
) { (result: Result<APIResponse<MemberProfile>, NetworkError>) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
switch result {
|
||||||
|
case .success(let response):
|
||||||
|
self.memberProfile = response.data
|
||||||
|
print("✅ 成功获取会员信息:", response.data)
|
||||||
|
print("✅ 用户ID:", response.data.userInfo.userId)
|
||||||
|
print("✅ 用户昵称:", response.data.userInfo.nickname)
|
||||||
|
print("✅ 用户邮箱:", response.data.userInfo.email)
|
||||||
|
case .failure(let error):
|
||||||
|
print("❌ 获取会员信息失败:", error)
|
||||||
|
// Optionally show an error message to the user
|
||||||
|
self.errorText = "Failed to load member info: \(error.localizedDescription)"
|
||||||
|
self.showErrorAlert = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user