feat: 订阅卡片
This commit is contained in:
parent
da842c8e7c
commit
8d5d69fb4a
@ -7,6 +7,8 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
AB6693CA2E65C94400BCAAC1 /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = AB6693C92E65C94400BCAAC1 /* SVGKit */; };
|
||||||
|
AB6693CC2E65C94400BCAAC1 /* SVGKitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = AB6693CB2E65C94400BCAAC1 /* SVGKitSwift */; };
|
||||||
AB8773632E4E04400071CB53 /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = AB8773622E4E040E0071CB53 /* .gitignore */; };
|
AB8773632E4E04400071CB53 /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = AB8773622E4E040E0071CB53 /* .gitignore */; };
|
||||||
ABC150C12E5DB39A00A1F970 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = ABC150C02E5DB39A00A1F970 /* Lottie */; };
|
ABC150C12E5DB39A00A1F970 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = ABC150C02E5DB39A00A1F970 /* Lottie */; };
|
||||||
ABE8998E2E533A7100CD7BA6 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = ABE8998D2E533A7100CD7BA6 /* Alamofire */; };
|
ABE8998E2E533A7100CD7BA6 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = ABE8998D2E533A7100CD7BA6 /* Alamofire */; };
|
||||||
@ -57,6 +59,8 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
AB6693CC2E65C94400BCAAC1 /* SVGKitSwift in Frameworks */,
|
||||||
|
AB6693CA2E65C94400BCAAC1 /* SVGKit in Frameworks */,
|
||||||
ABE8998E2E533A7100CD7BA6 /* Alamofire in Frameworks */,
|
ABE8998E2E533A7100CD7BA6 /* Alamofire in Frameworks */,
|
||||||
ABC150C12E5DB39A00A1F970 /* Lottie in Frameworks */,
|
ABC150C12E5DB39A00A1F970 /* Lottie in Frameworks */,
|
||||||
);
|
);
|
||||||
@ -114,6 +118,8 @@
|
|||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
ABE8998D2E533A7100CD7BA6 /* Alamofire */,
|
ABE8998D2E533A7100CD7BA6 /* Alamofire */,
|
||||||
ABC150C02E5DB39A00A1F970 /* Lottie */,
|
ABC150C02E5DB39A00A1F970 /* Lottie */,
|
||||||
|
AB6693C92E65C94400BCAAC1 /* SVGKit */,
|
||||||
|
AB6693CB2E65C94400BCAAC1 /* SVGKitSwift */,
|
||||||
);
|
);
|
||||||
productName = wake;
|
productName = wake;
|
||||||
productReference = ABB4E2082E4B75D900660198 /* wake.app */;
|
productReference = ABB4E2082E4B75D900660198 /* wake.app */;
|
||||||
@ -146,6 +152,7 @@
|
|||||||
packageReferences = (
|
packageReferences = (
|
||||||
ABE8998C2E533A7100CD7BA6 /* XCRemoteSwiftPackageReference "Alamofire" */,
|
ABE8998C2E533A7100CD7BA6 /* XCRemoteSwiftPackageReference "Alamofire" */,
|
||||||
ABC150BF2E5DB39A00A1F970 /* XCRemoteSwiftPackageReference "lottie-spm" */,
|
ABC150BF2E5DB39A00A1F970 /* XCRemoteSwiftPackageReference "lottie-spm" */,
|
||||||
|
AB6693C82E65C94400BCAAC1 /* XCRemoteSwiftPackageReference "SVGKit" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = ABB4E2092E4B75D900660198 /* Products */;
|
productRefGroup = ABB4E2092E4B75D900660198 /* Products */;
|
||||||
@ -392,6 +399,14 @@
|
|||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
AB6693C82E65C94400BCAAC1 /* XCRemoteSwiftPackageReference "SVGKit" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/SVGKit/SVGKit.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 3.0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
ABC150BF2E5DB39A00A1F970 /* XCRemoteSwiftPackageReference "lottie-spm" */ = {
|
ABC150BF2E5DB39A00A1F970 /* XCRemoteSwiftPackageReference "lottie-spm" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/airbnb/lottie-spm.git";
|
repositoryURL = "https://github.com/airbnb/lottie-spm.git";
|
||||||
@ -411,6 +426,16 @@
|
|||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
AB6693C92E65C94400BCAAC1 /* SVGKit */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = AB6693C82E65C94400BCAAC1 /* XCRemoteSwiftPackageReference "SVGKit" */;
|
||||||
|
productName = SVGKit;
|
||||||
|
};
|
||||||
|
AB6693CB2E65C94400BCAAC1 /* SVGKitSwift */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = AB6693C82E65C94400BCAAC1 /* XCRemoteSwiftPackageReference "SVGKit" */;
|
||||||
|
productName = SVGKitSwift;
|
||||||
|
};
|
||||||
ABC150C02E5DB39A00A1F970 /* Lottie */ = {
|
ABC150C02E5DB39A00A1F970 /* Lottie */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = ABC150BF2E5DB39A00A1F970 /* XCRemoteSwiftPackageReference "lottie-spm" */;
|
package = ABC150BF2E5DB39A00A1F970 /* XCRemoteSwiftPackageReference "lottie-spm" */;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "0e95cd18402f001189cea942918f7d0c4c8b04175c6c482029650c892d28d55a",
|
"originHash" : "d4b9379b4bd658fe79a6ae528c96d3386427dfe9d23635a65dad6edf12af85ff",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "alamofire",
|
"identity" : "alamofire",
|
||||||
@ -10,6 +10,15 @@
|
|||||||
"version" : "5.10.2"
|
"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",
|
"identity" : "lottie-spm",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@ -18,6 +27,24 @@
|
|||||||
"revision" : "04f2fd18cc9404a0a0917265a449002674f24ec9",
|
"revision" : "04f2fd18cc9404a0a0917265a449002674f24ec9",
|
||||||
"version" : "4.5.2"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 3
|
"version" : 3
|
||||||
|
|||||||
@ -182,6 +182,8 @@ struct BlindBoxView: View {
|
|||||||
let mediaType: BlindBoxMediaType
|
let mediaType: BlindBoxMediaType
|
||||||
@State private var showModal = false // 控制用户资料弹窗显示
|
@State private var showModal = false // 控制用户资料弹窗显示
|
||||||
@State private var showSettings = false // 控制设置页面显示
|
@State private var showSettings = false // 控制设置页面显示
|
||||||
|
@State private var isMember = false // 是否是会员
|
||||||
|
@State private var memberDate = "" // 会员到期时间
|
||||||
@State private var showLogin = false
|
@State private var showLogin = false
|
||||||
@State private var memberProfile: MemberProfile? = nil
|
@State private var memberProfile: MemberProfile? = nil
|
||||||
@State private var blindCount: BlindCount? = nil
|
@State private var blindCount: BlindCount? = nil
|
||||||
@ -275,6 +277,8 @@ struct BlindBoxView: View {
|
|||||||
switch result {
|
switch result {
|
||||||
case .success(let response):
|
case .success(let response):
|
||||||
self.memberProfile = response.data
|
self.memberProfile = response.data
|
||||||
|
self.isMember = response.data.membershipLevel == "pioneer"
|
||||||
|
self.memberDate = response.data.membershipEndAt ?? ""
|
||||||
print("✅ 成功获取会员信息:", response.data)
|
print("✅ 成功获取会员信息:", response.data)
|
||||||
print("✅ 用户ID:", response.data.userInfo.userId)
|
print("✅ 用户ID:", response.data.userInfo.userId)
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
@ -705,13 +709,9 @@ struct BlindBoxView: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
// 1. 背景SVG
|
// 1. 背景SVG
|
||||||
if !showScalingOverlay {
|
if !showScalingOverlay {
|
||||||
SVGImage(svgName: "BlindBg")
|
SVGImage(svgName: "BlindBg", contentMode: .fit)
|
||||||
.frame(
|
// .position(x: UIScreen.main.bounds.width / 2,
|
||||||
width: UIScreen.main.bounds.width * 1.8,
|
// y: UIScreen.main.bounds.height * 0.325)
|
||||||
height: UIScreen.main.bounds.height * 0.85
|
|
||||||
)
|
|
||||||
.position(x: UIScreen.main.bounds.width / 2,
|
|
||||||
y: UIScreen.main.bounds.height * 0.325)
|
|
||||||
.opacity(showScalingOverlay ? 0 : 1)
|
.opacity(showScalingOverlay ? 0 : 1)
|
||||||
.animation(.easeOut(duration: 1.5), value: showScalingOverlay)
|
.animation(.easeOut(duration: 1.5), value: showScalingOverlay)
|
||||||
}
|
}
|
||||||
@ -830,6 +830,7 @@ struct BlindBoxView: View {
|
|||||||
.offset(x: -10, y: UIScreen.main.bounds.height * 0.2)
|
.offset(x: -10, y: UIScreen.main.bounds.height * 0.2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding()
|
||||||
.frame(
|
.frame(
|
||||||
maxWidth: .infinity,
|
maxWidth: .infinity,
|
||||||
maxHeight: UIScreen.main.bounds.height * 0.65
|
maxHeight: UIScreen.main.bounds.height * 0.65
|
||||||
@ -897,7 +898,9 @@ struct BlindBoxView: View {
|
|||||||
) {
|
) {
|
||||||
UserProfileModal(
|
UserProfileModal(
|
||||||
showModal: $showModal,
|
showModal: $showModal,
|
||||||
showSettings: $showSettings
|
showSettings: $showSettings,
|
||||||
|
isMember: $isMember,
|
||||||
|
memberDate: $memberDate
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.shadow(color: .black.opacity(0.3), radius: 10, x: 5, y: 0)
|
.shadow(color: .black.opacity(0.3), radius: 10, x: 5, y: 0)
|
||||||
|
|||||||
@ -1,92 +1,144 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WebKit
|
import SVGKit
|
||||||
|
|
||||||
struct SVGImage: UIViewRepresentable {
|
struct SVGImage: UIViewRepresentable {
|
||||||
let svgName: String
|
let svgName: String
|
||||||
var shouldFill: Bool = false
|
var contentMode: ContentMode = .fit
|
||||||
|
var tintColor: Color?
|
||||||
|
|
||||||
func makeUIView(context: Context) -> WKWebView {
|
private var svgPath: String {
|
||||||
let webView = WKWebView()
|
return svgName
|
||||||
webView.isOpaque = false
|
|
||||||
webView.backgroundColor = .clear
|
|
||||||
webView.scrollView.isScrollEnabled = false
|
|
||||||
webView.scrollView.contentInsetAdjustmentBehavior = .never
|
|
||||||
|
|
||||||
guard let path = Bundle.main.path(forResource: svgName, ofType: "svg") else {
|
|
||||||
print("❌ 无法找到 SVG 文件: \(svgName).svg")
|
|
||||||
return webView
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileURL = URL(fileURLWithPath: path)
|
|
||||||
|
|
||||||
let svgStyle = shouldFill ? """
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
""" : """
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
"""
|
|
||||||
|
|
||||||
let htmlString = """
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
||||||
<style>
|
|
||||||
body, html {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
svg {
|
|
||||||
\(svgStyle)
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<object type="image/svg+xml" data="\(fileURL.lastPathComponent)"></object>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
webView.loadHTMLString(htmlString, baseURL: fileURL.deletingLastPathComponent())
|
|
||||||
return webView
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ uiView: WKWebView, context: Context) {}
|
private func createImageView() -> SVGKFastImageView {
|
||||||
|
let emptySVGString = """
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1" preserveAspectRatio="xMidYMid meet">
|
||||||
|
<rect width="1" height="1" fill="red" opacity="0.5"/>
|
||||||
|
</svg>
|
||||||
|
"""
|
||||||
|
|
||||||
func sizeThatFits(_ proposal: ProposedViewSize, uiView: WKWebView, context: Context) -> CGSize? {
|
if let data = emptySVGString.data(using: .utf8),
|
||||||
|
let svgImage = SVGKImage(data: data) {
|
||||||
|
let imageView = SVGKFastImageView(svgkImage: svgImage) ?? SVGKFastImageView()
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
return imageView
|
||||||
|
}
|
||||||
|
|
||||||
|
return SVGKFastImageView()
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> SVGKFastImageView {
|
||||||
|
print("🔄 开始加载SVG: \(svgName)")
|
||||||
|
let imageView = createImageView()
|
||||||
|
loadSVG(into: imageView)
|
||||||
|
configureView(imageView)
|
||||||
|
return imageView
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadSVG(into imageView: SVGKFastImageView) {
|
||||||
|
guard let path = Bundle.main.path(forResource: svgPath, ofType: "svg") else {
|
||||||
|
print("⚠️ 在main bundle中找不到文件: \(svgPath).svg")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = URL(fileURLWithPath: path)
|
||||||
|
guard let svgImage = SVGKImage(contentsOf: url) else {
|
||||||
|
print("❌ 无法从URL创建SVG: \(path)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置SVG的尺寸为容器大小
|
||||||
|
let containerSize = imageView.bounds.size
|
||||||
|
if containerSize != .zero {
|
||||||
|
svgImage.size = containerSize
|
||||||
|
} else {
|
||||||
|
// 如果容器大小未知,设置一个默认大小
|
||||||
|
svgImage.size = CGSize(width: 100, height: 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
print("✅ 成功加载SVG: \(svgName), 尺寸: \(svgImage.size)")
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
imageView.image = svgImage
|
||||||
|
imageView.setNeedsLayout()
|
||||||
|
imageView.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureView(_ imageView: SVGKFastImageView) {
|
||||||
|
imageView.contentMode = contentMode == .fit ? .scaleAspectFit : .scaleAspectFill
|
||||||
|
imageView.clipsToBounds = true
|
||||||
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
// 确保图片视图可以正确缩放
|
||||||
|
imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
|
||||||
|
if let tintColor = tintColor?.uiColor {
|
||||||
|
imageView.tintColor = tintColor
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.applyTintColor(tintColor, to: imageView.layer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyTintColor(_ color: UIColor, to layer: CALayer) {
|
||||||
|
if let shapeLayer = layer as? CAShapeLayer {
|
||||||
|
shapeLayer.fillColor = color.cgColor
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.sublayers?.forEach { sublayer in
|
||||||
|
applyTintColor(color, to: sublayer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: SVGKFastImageView, context: Context) {
|
||||||
|
loadSVG(into: uiView)
|
||||||
|
|
||||||
|
if let tintColor = tintColor?.uiColor {
|
||||||
|
uiView.tintColor = tintColor
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.applyTintColor(tintColor, to: uiView.layer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uiView.contentMode = contentMode == .fit ? .scaleAspectFit : .scaleAspectFill
|
||||||
|
}
|
||||||
|
|
||||||
|
func sizeThatFits(_ proposal: ProposedViewSize, uiView: SVGKFastImageView, context: Context) -> CGSize? {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - ContentMode
|
||||||
|
extension SVGImage {
|
||||||
|
enum ContentMode {
|
||||||
|
case fit // 保持宽高比,适应容器
|
||||||
|
case fill // 保持宽高比,填充容器(可能被裁剪)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Preview
|
// MARK: - Preview
|
||||||
#Preview {
|
#Preview {
|
||||||
VStack {
|
VStack(spacing: 20) {
|
||||||
Text("Filled SVG")
|
Text("IP SVG")
|
||||||
SVGImage(svgName: "YourSVGName", shouldFill: true)
|
SVGImage(svgName: "IP")
|
||||||
.frame(width: 200, height: 100)
|
.frame(width: 100, height: 100)
|
||||||
.background(Color.gray.opacity(0.2))
|
.background(Color.gray.opacity(0.2))
|
||||||
|
.border(Color.red, width: 1)
|
||||||
|
|
||||||
Text("Intrinsic Size SVG")
|
Text("Pioneer SVG")
|
||||||
SVGImage(svgName: "YourSVGName", shouldFill: false)
|
SVGImage(svgName: "Pioneer", contentMode: .fill)
|
||||||
.frame(width: 200, height: 100)
|
.frame(width: 100, height: 50)
|
||||||
.background(Color.gray.opacity(0.2))
|
.background(Color.gray.opacity(0.2))
|
||||||
|
.border(Color.blue, width: 1)
|
||||||
|
.clipped()
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Color Extension
|
||||||
|
private extension Color {
|
||||||
|
var uiColor: UIColor {
|
||||||
|
return UIColor(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -25,11 +25,20 @@ struct APIResponse<T: Codable>: Codable {
|
|||||||
struct UserProfileModal: View {
|
struct UserProfileModal: View {
|
||||||
@Binding var showModal: Bool
|
@Binding var showModal: Bool
|
||||||
@Binding var showSettings: Bool
|
@Binding var showSettings: Bool
|
||||||
|
@Binding var isMember: Bool
|
||||||
|
@Binding var memberDate: String
|
||||||
@State private var userProfile: UserProfile?
|
@State private var userProfile: UserProfile?
|
||||||
@State private var isLoading = false
|
@State private var isLoading = false
|
||||||
@State private var errorMessage: String?
|
@State private var errorMessage: String?
|
||||||
@State private var isCopied = false
|
@State private var isCopied = false
|
||||||
|
|
||||||
|
init(showModal: Binding<Bool>, showSettings: Binding<Bool>, isMember: Binding<Bool>, memberDate: Binding<String>) {
|
||||||
|
self._showModal = showModal
|
||||||
|
self._showSettings = showSettings
|
||||||
|
self._isMember = isMember
|
||||||
|
self._memberDate = memberDate
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -124,43 +133,8 @@ struct UserProfileModal: View {
|
|||||||
)
|
)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|
||||||
Button(action: {
|
// 当前订阅状态卡片
|
||||||
Router.shared.navigate(to: .subscribe)
|
currentSubscriptionCard
|
||||||
}) {
|
|
||||||
ZStack(alignment: .center) {
|
|
||||||
// SVG背景 - 确保铺满整个区域
|
|
||||||
SVGImage(svgName: "Pioneer")
|
|
||||||
.scaledToFill()
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
|
|
||||||
// 内容区域
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
|
||||||
Text("Pioneer")
|
|
||||||
.font(Typography.font(for: .title3))
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.foregroundColor(.themeTextMessageMain)
|
|
||||||
.padding(.top, 6)
|
|
||||||
|
|
||||||
Text("Expires on :")
|
|
||||||
.font(.system(size: 12))
|
|
||||||
.foregroundColor(.themeTextMessageMain)
|
|
||||||
.padding(.top, 2)
|
|
||||||
|
|
||||||
Text("March 15, 2025")
|
|
||||||
.font(.system(size: 12))
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.foregroundColor(.themeTextMessageMain)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(.leading, 32)
|
|
||||||
// 可以添加内边距使内容不紧贴边缘
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
}
|
|
||||||
.frame(height: 112)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.cornerRadius(16)
|
|
||||||
.clipped() // 确保内容不会超出边界
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
// upload
|
// upload
|
||||||
@ -270,6 +244,30 @@ struct UserProfileModal: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - 当前订阅状态卡片
|
||||||
|
private var currentSubscriptionCard: some View {
|
||||||
|
let status: SubscriptionStatus = {
|
||||||
|
if isMember {
|
||||||
|
let dateFormatter = ISO8601DateFormatter()
|
||||||
|
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||||
|
let expiryDate = dateFormatter.date(from: memberDate) ?? Date()
|
||||||
|
return .pioneer(expiryDate: expiryDate)
|
||||||
|
} else {
|
||||||
|
return .free
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return SubscriptionStatusBar(
|
||||||
|
status: status,
|
||||||
|
height: 112,
|
||||||
|
onSubscribeTap: {
|
||||||
|
// 跳转到订阅页面
|
||||||
|
Router.shared.navigate(to: .subscribe)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(.horizontal, Theme.Spacing.xl)
|
||||||
|
}
|
||||||
|
|
||||||
private func fetchUserInfo() {
|
private func fetchUserInfo() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
errorMessage = nil
|
errorMessage = nil
|
||||||
@ -295,5 +293,5 @@ struct UserProfileModal: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
UserProfileModal(showModal: .constant(true), showSettings: .constant(false))
|
UserProfileModal(showModal: .constant(true), showSettings: .constant(false), isMember: .constant(true), memberDate: .constant(""))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,16 +53,18 @@ enum SubscriptionStatus {
|
|||||||
struct SubscriptionStatusBar: View {
|
struct SubscriptionStatusBar: View {
|
||||||
let status: SubscriptionStatus
|
let status: SubscriptionStatus
|
||||||
let onSubscribeTap: (() -> Void)?
|
let onSubscribeTap: (() -> Void)?
|
||||||
|
private let height: CGFloat
|
||||||
|
|
||||||
init(status: SubscriptionStatus, onSubscribeTap: (() -> Void)? = nil) {
|
init(status: SubscriptionStatus, height: CGFloat? = nil, onSubscribeTap: (() -> Void)? = nil) {
|
||||||
self.status = status
|
self.status = status
|
||||||
|
self.height = height ?? 155 // 默认高度为155
|
||||||
self.onSubscribeTap = onSubscribeTap
|
self.onSubscribeTap = onSubscribeTap
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
// Background SVG - First layer
|
// Background SVG - First layer
|
||||||
SVGImage(svgName: status.backgroundImageName, shouldFill: true)
|
SVGImage(svgName: status.backgroundImageName)
|
||||||
.frame(maxWidth: .infinity, minHeight: 120)
|
.frame(maxWidth: .infinity, minHeight: 120)
|
||||||
.clipped()
|
.clipped()
|
||||||
|
|
||||||
@ -75,17 +77,20 @@ struct SubscriptionStatusBar: View {
|
|||||||
.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)
|
.padding(.leading, 24)
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 10)
|
||||||
// Expiry date or subscribe button
|
// 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: 12))
|
||||||
.foregroundColor(status.textColor.opacity(0.9))
|
.foregroundColor(.themeTextMessageMain)
|
||||||
|
.padding(.top, 2)
|
||||||
|
|
||||||
Text(formatDate(expiryDate))
|
Text(formatDate(expiryDate))
|
||||||
.font(.system(size: 16, weight: .semibold))
|
.font(.system(size: 12))
|
||||||
.foregroundColor(status.textColor)
|
.fontWeight(.bold)
|
||||||
|
.foregroundColor(.themeTextMessageMain)
|
||||||
}
|
}
|
||||||
.padding(.leading, 24)
|
.padding(.leading, 24)
|
||||||
.padding(.bottom, 20)
|
.padding(.bottom, 20)
|
||||||
@ -93,10 +98,14 @@ struct SubscriptionStatusBar: View {
|
|||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
|
||||||
}
|
}
|
||||||
.frame(height: 155)
|
.frame(height: height) // 使用传入的高度或默认高度
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.cornerRadius(20)
|
.cornerRadius(20)
|
||||||
.clipped()
|
.clipped()
|
||||||
|
.contentShape(Rectangle()) // Make entire area tappable
|
||||||
|
.onTapGesture {
|
||||||
|
onSubscribeTap?()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 日期格式化
|
// MARK: - 日期格式化
|
||||||
|
|||||||
@ -139,7 +139,7 @@ struct SubscribeView: View {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
return SubscriptionStatusBar(
|
return SubscriptionStatusBar(
|
||||||
status: .pioneer(expiryDate: Date()),
|
status: status,
|
||||||
onSubscribeTap: {
|
onSubscribeTap: {
|
||||||
// 订阅操作
|
// 订阅操作
|
||||||
handleSubscribe()
|
handleSubscribe()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user