Add CI/CD configuration and API documentation
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"v":"5.12.2","fr":60,"ip":0,"op":72,"w":500,"h":500,"nm":"Lock","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Icon","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,247,0],"ix":2,"l":2},"a":{"a":0,"k":[-81.441,794.705,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[550,550,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[360,360,100]},{"t":30,"s":[550,550,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-2.851,0],[0,-2.851]],"o":[[0,0],[0,0],[0,-2.851],[2.851,0],[0,0]],"v":[[-76.265,790.525],[-86.617,790.525],[-86.617,787.34],[-81.441,782.164],[-76.265,787.34]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.494,-0.366],[0,0],[0.653,0],[0,0.653],[0,0],[0,0.637],[-1.099,0],[0,0],[0,-1.099]],"o":[[0,0],[0,0.653],[-0.653,0],[0,0],[-0.462,-0.366],[0,-1.099],[0,0],[1.099,0],[0,0.653]],"v":[[-80.247,800.065],[-80.247,803.266],[-81.441,804.46],[-82.636,803.266],[-82.636,800.017],[-83.416,798.488],[-81.425,796.497],[-81.409,796.497],[-79.418,798.488]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[2.389,0.366],[0,0],[4.173,0],[0,-4.173],[0,0],[0,-2.755],[0,0],[-3.185,0],[0,0],[0,3.185],[0,0]],"o":[[0,0],[0,-4.173],[-4.173,0],[0,0],[-2.389,0.366],[0,0],[0,3.185],[0,0],[3.185,0],[0,0],[0,-2.755]],"v":[[-73.876,790.62],[-73.876,787.34],[-81.441,779.775],[-89.006,787.34],[-89.006,790.62],[-92.589,795.303],[-92.589,804.859],[-87.812,809.636],[-75.071,809.636],[-70.293,804.859],[-70.293,795.303]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[530,530,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":5,"s":[520,520,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[550,550,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[520,520,100]},{"t":50,"s":[530,530,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-16.335],[16.334,0],[0,16.334],[-16.335,0]],"o":[[0,16.334],[-16.335,0],[0,-16.335],[16.334,0]],"v":[[29.576,0],[0,29.576],[-29.576,0],[0,-29.576]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.29,0.937,0.435,0.5,0.145,0.718,0.437,1,0,0.498,0.439],"ix":9}},"s":{"a":0,"k":[-11.294,-32.337],"ix":5},"e":{"a":0,"k":[9.315,28.16],"ix":6},"t":1,"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"v":"5.9.3","fr":29.9700012207031,"ip":0,"op":60.0000024438501,"w":1920,"h":1080,"nm":"1_Loading_Line","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Blue Line","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,539,0],"ix":2,"l":2},"a":{"a":0,"k":[0,-1,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[800,800],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.212477396049,0.540471813725,0.898039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":80,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-1],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.9],"y":[0]},"t":15,"s":[0]},{"t":60.0000024438501,"s":[99.9]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.9],"y":[0]},"t":0,"s":[0.01]},{"t":45.0000018328876,"s":[100]}],"ix":2},"o":{"a":0,"k":-1,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150.000006109625,"st":0,"ct":1,"bm":0}],"markers":[{"tm":60.0000024438501,"cm":"END (No Loop)","dr":0}]}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// ChatViewControllerWrapper.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import Crisp
|
||||
|
||||
// Step 2: Create a SwiftUI wrapper for ChatViewController
|
||||
struct ChatViewControllerWrapper: UIViewControllerRepresentable {
|
||||
|
||||
// Initialize any properties needed to pass to ChatViewController
|
||||
typealias UIViewControllerType = ChatViewController
|
||||
|
||||
func makeUIViewController(context: Context) -> ChatViewController {
|
||||
// Return the ChatViewController
|
||||
let chatViewController = ChatViewController()
|
||||
return chatViewController
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: ChatViewController, context: Context) {
|
||||
// Update the view controller when needed
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// CustomCorners.swift
|
||||
// CustomCorners
|
||||
//
|
||||
// Created by zues on 12/09/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CustomCorners: Shape {
|
||||
|
||||
var radius: CGFloat
|
||||
var corners: UIRectCorner
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
|
||||
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
|
||||
|
||||
return Path(path.cgPath)
|
||||
}
|
||||
}
|
||||
|
||||
func getSafeAreaInsets() -> UIEdgeInsets {
|
||||
let windowScene = UIApplication.shared.connectedScenes
|
||||
.filter({ $0.activationState == .foregroundActive })
|
||||
.map({ $0 as? UIWindowScene })
|
||||
.compactMap({ $0 })
|
||||
.first
|
||||
|
||||
guard let keyWindow = windowScene?.windows.filter({ $0.isKeyWindow }).first else {
|
||||
return .zero
|
||||
}
|
||||
|
||||
return keyWindow.safeAreaInsets
|
||||
}
|
||||
|
||||
|
||||
func getRect() -> CGRect {
|
||||
return UIScreen.main.bounds
|
||||
}
|
||||
@ViewBuilder
|
||||
func BackgroundBg()->some View{
|
||||
|
||||
|
||||
|
||||
ZStack{
|
||||
|
||||
LinearGradient(colors: [
|
||||
|
||||
Color("BG1"),
|
||||
Color("BG1"),
|
||||
Color("BG2"),
|
||||
Color("BG2"),
|
||||
|
||||
], startPoint: .top, endPoint: .bottom)
|
||||
|
||||
|
||||
// Little Planet and little stars....
|
||||
Image("mars")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 30, height: 30)
|
||||
.scaleEffect(getRect().height < 750 ? 0.8 : 1)
|
||||
// not using offset...
|
||||
// using postiton..
|
||||
// this will position the object using screen basis...
|
||||
.position(x: 50, y: getRect().height < 750 ? 200 : 220)
|
||||
.opacity(0.7)
|
||||
|
||||
// Sample star points....
|
||||
let stars: [CGPoint] = [
|
||||
|
||||
CGPoint(x: 15, y: 190),
|
||||
CGPoint(x: 25, y: 250),
|
||||
CGPoint(x: 20, y: 350),
|
||||
CGPoint(x: getRect().width - 30, y: 240),
|
||||
]
|
||||
|
||||
ForEach(stars,id: \.x){star in
|
||||
|
||||
Circle()
|
||||
.fill(.white.opacity(0.3))
|
||||
.frame(width: 5, height: 5)
|
||||
.position(star)
|
||||
.offset(y: getRect().height < 750 ? -20 : 0)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// HTMLTextView.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct HTMLTextView: UIViewRepresentable {
|
||||
let htmlString: String
|
||||
|
||||
func makeUIView(context: Context) -> UITextView {
|
||||
let textView = UITextView()
|
||||
textView.isEditable = false
|
||||
textView.isScrollEnabled = true
|
||||
textView.backgroundColor = .clear
|
||||
return textView
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UITextView, context: Context) {
|
||||
guard let data = htmlString.data(using: .utf8) else { return }
|
||||
do {
|
||||
|
||||
let attributedString = try NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil)
|
||||
uiView.attributedText = attributedString
|
||||
} catch {
|
||||
print("Error loading HTML: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// CustomTF.swift
|
||||
// LoginKit
|
||||
//
|
||||
// Created by zues on 04/08/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CustomTF: View {
|
||||
var sfIcon: String
|
||||
var iconTint: Color = .gray
|
||||
var hint: String
|
||||
/// Hides TextField
|
||||
var isPassword: Bool = false
|
||||
var hasDriveLine: Bool = true
|
||||
|
||||
|
||||
@Binding var value: String
|
||||
/// View Properties
|
||||
@State private var showPassword: Bool = false
|
||||
/// When Switching Between Hide/Reveal Password Field, The Keyboard is Closing, to avoid that using the FocusState
|
||||
@FocusState private var passwordState: HideState?
|
||||
|
||||
enum HideState {
|
||||
case hide
|
||||
case reveal
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 8, content: {
|
||||
Image(systemName: sfIcon)
|
||||
.foregroundStyle(iconTint)
|
||||
/// Since I Need Same Width to Align TextFields Equally
|
||||
.frame(width: 30)
|
||||
/// Slightly Bringing Down
|
||||
.offset(y: 2)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8, content: {
|
||||
if isPassword {
|
||||
Group {
|
||||
/// Revealing Password when users wants to show Password
|
||||
if showPassword {
|
||||
TextField(hint, text: $value)
|
||||
.focused($passwordState, equals: .reveal)
|
||||
} else {
|
||||
SecureField(hint, text: $value)
|
||||
.focused($passwordState, equals: .hide)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TextField(hint, text: $value)
|
||||
}
|
||||
|
||||
if hasDriveLine {
|
||||
Divider()
|
||||
}
|
||||
|
||||
})
|
||||
.overlay(alignment: .trailing) {
|
||||
/// Password Reveal Button
|
||||
if isPassword {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
showPassword.toggle()
|
||||
}
|
||||
passwordState = showPassword ? .reveal : .hide
|
||||
}, label: {
|
||||
Image(systemName: showPassword ? "eye.slash" : "eye")
|
||||
.foregroundStyle(.gray)
|
||||
.padding(10)
|
||||
.contentShape(.rect)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// GradientButton.swift
|
||||
// LoginKit
|
||||
//
|
||||
// Created by zues on 04/08/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct GradientButton: View {
|
||||
var title: String
|
||||
var icon: String
|
||||
var onClick: () -> ()
|
||||
var body: some View {
|
||||
Button(action: onClick, label: {
|
||||
HStack(spacing: 15) {
|
||||
Text(title)
|
||||
Image(systemName: icon)
|
||||
}
|
||||
////.fontWeight(.bold)
|
||||
.foregroundStyle(.white)
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 35)
|
||||
.background(.linearGradient(colors: [.main, .main2, ], startPoint: .top, endPoint: .bottom), in: .capsule)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// OTPVerificationView.swift
|
||||
// AutoOtpTF
|
||||
//
|
||||
// Created by zues on 23/12/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct OTPVerificationView: View {
|
||||
/// - View Properties
|
||||
@Binding var otpText: String
|
||||
/// - Keyboard State
|
||||
@FocusState private var isKeyboardShowing: Bool
|
||||
var body: some View {
|
||||
HStack(spacing: 0){
|
||||
/// - OTP Text Boxes
|
||||
/// Change Count Based on your OTP Text Size
|
||||
ForEach(0..<6,id: \.self){index in
|
||||
OTPTextBox(index)
|
||||
}
|
||||
}
|
||||
.background(content: {
|
||||
TextField("", text: $otpText.limit(6))
|
||||
.keyboardType(.numberPad)
|
||||
.textContentType(.oneTimeCode)
|
||||
/// - Hiding it Out
|
||||
.frame(width: 1, height: 1)
|
||||
.opacity(0.001)
|
||||
.blendMode(.screen)
|
||||
.focused($isKeyboardShowing)
|
||||
})
|
||||
.contentShape(Rectangle())
|
||||
/// - Opening Keyboard When Tapped
|
||||
.onTapGesture {
|
||||
isKeyboardShowing = true
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .keyboard) {
|
||||
Button("Done"){
|
||||
isKeyboardShowing = false
|
||||
}
|
||||
.tint(.white)
|
||||
////.fontWeight(.heavy)
|
||||
.hSpacing(.trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: OTP Text Box
|
||||
@ViewBuilder
|
||||
func OTPTextBox(_ index: Int)->some View{
|
||||
ZStack{
|
||||
if otpText.count > index{
|
||||
/// - Finding Char At Index
|
||||
let startIndex = otpText.startIndex
|
||||
let charIndex = otpText.index(startIndex, offsetBy: index)
|
||||
let charToString = String(otpText[charIndex])
|
||||
Text(charToString)
|
||||
}else{
|
||||
Text(" ")
|
||||
}
|
||||
}
|
||||
.frame(width: 45, height: 45)
|
||||
.background {
|
||||
/// - Highlighting Current Active Box
|
||||
let status = (isKeyboardShowing && otpText.count == index)
|
||||
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
||||
.stroke(status ? .main : Color.gray,lineWidth: status ? 3 : 0.5)
|
||||
/// - Adding Animation
|
||||
.animation(.easeInOut(duration: 0.2), value: isKeyboardShowing)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Binding <String> Extension
|
||||
extension Binding where Value == String{
|
||||
func limit(_ length: Int)->Self{
|
||||
if self.wrappedValue.count > length{
|
||||
DispatchQueue.main.async {
|
||||
self.wrappedValue = String(self.wrappedValue.prefix(length))
|
||||
}
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// LottieView.swift
|
||||
// VPNUI
|
||||
//
|
||||
// Created by Mac on 2024/9/26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
//import Lottie
|
||||
import SwiftUI
|
||||
import Lottie
|
||||
|
||||
struct LottieView: UIViewRepresentable {
|
||||
|
||||
var animationFileName: String
|
||||
let loopMode: LottieLoopMode
|
||||
|
||||
func updateUIView(_ uiView: UIViewType, context: Context) {
|
||||
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> Lottie.LottieAnimationView {
|
||||
let animationView = LottieAnimationView(name: animationFileName)
|
||||
animationView.loopMode = loopMode
|
||||
animationView.play()
|
||||
animationView.contentMode = .scaleAspectFit
|
||||
|
||||
return animationView
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// Unitity.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TimestampConverter {
|
||||
|
||||
// Function to convert a timestamp to a formatted date string
|
||||
static func convertTimestampToDateString(_ timestamp: Int, format: String = "yyyy-MM-dd HH:mm:ss") -> String {
|
||||
let timeInterval = TimeInterval(timestamp)
|
||||
let date = Date(timeIntervalSince1970: timeInterval)
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = format
|
||||
return dateFormatter.string(from: date)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// UniversalAlert.swift
|
||||
// CustomUniversalAlert
|
||||
//
|
||||
// Created by zues Venkatesh on 18/09/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// Alert Config
|
||||
struct AlertConfig {
|
||||
fileprivate var enableBackgroundBlur: Bool = true
|
||||
fileprivate var disableOutsideTap: Bool = true
|
||||
fileprivate var transitionType: TransitionType = .slide
|
||||
fileprivate var slideEdge: Edge = .bottom
|
||||
fileprivate var show: Bool = false
|
||||
fileprivate var showView: Bool = false
|
||||
|
||||
init(enableBackgroundBlur: Bool = true, disableOutsideTap: Bool = true, transitionType: TransitionType = .slide, slideEdge: Edge = .bottom) {
|
||||
self.enableBackgroundBlur = enableBackgroundBlur
|
||||
self.disableOutsideTap = disableOutsideTap
|
||||
self.transitionType = transitionType
|
||||
self.slideEdge = slideEdge
|
||||
}
|
||||
|
||||
/// TransitionType
|
||||
enum TransitionType {
|
||||
case slide
|
||||
case opacity
|
||||
}
|
||||
|
||||
/// Alert Present/Dismiss Methods
|
||||
mutating
|
||||
func present() {
|
||||
show = true
|
||||
}
|
||||
|
||||
mutating
|
||||
func dismiss() {
|
||||
show = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fileprivate struct AlertView<Content: View>: View {
|
||||
@Binding var config: AlertConfig
|
||||
/// View Tag
|
||||
var tag: Int
|
||||
@ViewBuilder var content: () -> Content
|
||||
/// View Properties
|
||||
@State private var showView: Bool = false
|
||||
var body: some View {
|
||||
GeometryReader(content: { geometry in
|
||||
if showView && config.transitionType == .slide {
|
||||
content()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.transition(.move(edge: config.slideEdge))
|
||||
} else {
|
||||
content()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.opacity(showView ? 1 : 0)
|
||||
}
|
||||
})
|
||||
.background {
|
||||
ZStack {
|
||||
if config.enableBackgroundBlur {
|
||||
Rectangle()
|
||||
.fill(.ultraThinMaterial)
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(.primary.opacity(0.25))
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.contentShape(.rect)
|
||||
.onTapGesture {
|
||||
if !config.disableOutsideTap {
|
||||
config.dismiss()
|
||||
}
|
||||
}
|
||||
.opacity(showView ? 1 : 0)
|
||||
}
|
||||
.onAppear(perform: {
|
||||
config.showView = true
|
||||
})
|
||||
// .onChange(of: config.showView) { oldValue, newValue in
|
||||
// withAnimation(.smooth(duration: 0.35, extraBounce: 0)) {
|
||||
// showView = newValue
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
//
|
||||
// View+Extensions.swift
|
||||
// LoginKit
|
||||
//
|
||||
// Created by zues on 04/08/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// Custom SwiftUI View Extensions
|
||||
extension View {
|
||||
/// View Alignments
|
||||
@ViewBuilder
|
||||
func hSpacing(_ alignment: Alignment = .center) -> some View {
|
||||
self
|
||||
.frame(maxWidth: .infinity, alignment: alignment)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func vSpacing(_ alignment: Alignment = .center) -> some View {
|
||||
self
|
||||
.frame(maxHeight: .infinity, alignment: alignment)
|
||||
}
|
||||
|
||||
/// Disable With Opacity
|
||||
@ViewBuilder
|
||||
func disableWithOpacity(_ condition: Bool) -> some View {
|
||||
self
|
||||
.disabled(condition)
|
||||
.opacity(condition ? 0.5 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Extedning View to get Screen Size and Frame....
|
||||
extension View{
|
||||
|
||||
func getRect()->CGRect{
|
||||
UIScreen.main.bounds
|
||||
}
|
||||
|
||||
func getSafeArea()->UIEdgeInsets{
|
||||
guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene else{
|
||||
return .zero
|
||||
}
|
||||
|
||||
guard let safeArea = screen.windows.first?.safeAreaInsets else{
|
||||
return .zero
|
||||
}
|
||||
|
||||
return safeArea
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Text {
|
||||
init(_ attributedString: NSAttributedString) {
|
||||
self.init("") // initial, empty Text
|
||||
|
||||
// scan the attributed string for distinctly attributed regions
|
||||
attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length),
|
||||
options: []) { (attrs, range, _) in
|
||||
let string = attributedString.attributedSubstring(from: range).string
|
||||
var text = Text(string)
|
||||
|
||||
// then, read applicable attributes and apply them to the Text
|
||||
|
||||
if let font = attrs[.font] as? UIFont {
|
||||
// this takes care of the majority of formatting - text size, font family,
|
||||
// font weight, if it's italic, etc.
|
||||
text = text.font(.init(font))
|
||||
}
|
||||
|
||||
if let color = attrs[.foregroundColor] as? UIColor {
|
||||
text = text.foregroundColor(Color(color))
|
||||
}
|
||||
|
||||
if let kern = attrs[.kern] as? CGFloat {
|
||||
text = text.kerning(kern)
|
||||
}
|
||||
|
||||
if #available(iOS 14.0, *) {
|
||||
if let tracking = attrs[.tracking] as? CGFloat {
|
||||
text = text.tracking(tracking)
|
||||
}
|
||||
}
|
||||
|
||||
if let strikethroughStyle = attrs[.strikethroughStyle] as? NSNumber,
|
||||
strikethroughStyle != 0 {
|
||||
if let strikethroughColor = (attrs[.strikethroughColor] as? UIColor) {
|
||||
text = text.strikethrough(true, color: Color(strikethroughColor))
|
||||
} else {
|
||||
text = text.strikethrough(true)
|
||||
}
|
||||
}
|
||||
|
||||
if let underlineStyle = attrs[.underlineStyle] as? NSNumber,
|
||||
underlineStyle != 0 {
|
||||
if let underlineColor = (attrs[.underlineColor] as? UIColor) {
|
||||
text = text.underline(true, color: Color(underlineColor))
|
||||
} else {
|
||||
text = text.underline(true)
|
||||
}
|
||||
}
|
||||
|
||||
if let baselineOffset = attrs[.baselineOffset] as? NSNumber {
|
||||
text = text.baselineOffset(CGFloat(baselineOffset.floatValue))
|
||||
}
|
||||
|
||||
// append the newly styled subtext to the rest of the text
|
||||
self = self + text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Text {
|
||||
init(html htmlString: String, // the HTML-formatted string
|
||||
raw: Bool = false, // set to true if you don't want to embed in the doc skeleton
|
||||
size: CGFloat? = nil, // optional document-wide text size
|
||||
fontFamily: String = "-apple-system") { // optional document-wide font family
|
||||
let fullHTML: String
|
||||
if raw {
|
||||
fullHTML = htmlString
|
||||
} else {
|
||||
var sizeCss = ""
|
||||
if let size = size {
|
||||
sizeCss = "font-size: \(size)px;"
|
||||
}
|
||||
fullHTML = """
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: \(fontFamily);
|
||||
\(sizeCss)
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
\(htmlString)
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
}
|
||||
let attributedString: NSAttributedString
|
||||
if let data = fullHTML.data(using: .unicode),
|
||||
let attrString = try? NSAttributedString(data: data,
|
||||
options: [.documentType: NSAttributedString.DocumentType.html],
|
||||
documentAttributes: nil) {
|
||||
attributedString = attrString
|
||||
} else {
|
||||
attributedString = NSAttributedString()
|
||||
}
|
||||
|
||||
self.init(attributedString) // uses the NSAttributedString initializer
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public extension View {
|
||||
// 扩展 View 以提供隐藏键盘的功能
|
||||
func hideKeyboard() {
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
}
|
||||
func transparentScrolling() -> some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
return scrollContentBackground(.hidden)
|
||||
} else {
|
||||
return onAppear {
|
||||
UITextView.appearance().backgroundColor = .clear
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TriangleShape: Shape {
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var path = Path()
|
||||
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
|
||||
path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
|
||||
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// InviteReponse.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Welcome
|
||||
struct InviteReponse: Codable {
|
||||
let status, message: String?
|
||||
let data: InviteReponseDataClass?
|
||||
}
|
||||
|
||||
// MARK: - DataClass
|
||||
struct InviteReponseDataClass: Codable {
|
||||
let codes: [InviteCode]?
|
||||
let stat: [Int]?
|
||||
}
|
||||
|
||||
// MARK: - Code
|
||||
struct InviteCode: Codable,Identifiable {
|
||||
var id = UUID().uuidString
|
||||
let userID: Int
|
||||
let code: String
|
||||
let createdAt:Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case userID = "user_id"
|
||||
case code
|
||||
case createdAt = "created_at"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// File.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/13.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct MessageResponse: Codable {
|
||||
let message: String
|
||||
let status: String?
|
||||
let errors: MessageResponseErrors?
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
struct MessageResponseErrors: Codable {
|
||||
let email: [String]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// Nodes.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/13.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
// MARK: - Welcome
|
||||
struct nodereponse: Codable {
|
||||
let data: [nodereponseData]?
|
||||
let message: String?
|
||||
}
|
||||
|
||||
// MARK: - Datum
|
||||
struct nodereponseData: Codable,Identifiable {
|
||||
// let id: Int
|
||||
var id = UUID().uuidString
|
||||
|
||||
let type, name, rate: String
|
||||
let id2, isOnline: Int
|
||||
let cacheKey: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case type, name, rate
|
||||
case isOnline = "is_online"
|
||||
case id2 = "id"
|
||||
case cacheKey = "cache_key"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// OnBoardingItem.swift
|
||||
// AnimatedOnBoardingScreen
|
||||
//
|
||||
// Created by zues on 17/12/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Lottie
|
||||
|
||||
// MARK: OnBoarding Item Model
|
||||
struct OnboardingItem: Identifiable,Equatable{
|
||||
var id: UUID = .init()
|
||||
var title: String
|
||||
var subTitle: String
|
||||
var lottieView: LottieAnimationView = .init()
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
//
|
||||
// File.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - OrderInfoReponse
|
||||
struct OrderInfoReponse: Codable {
|
||||
let status, message: String?
|
||||
let data: [OrderInfoDatum]?
|
||||
}
|
||||
|
||||
struct OrderInfoSingleReponse: Codable {
|
||||
let status, message: String?
|
||||
let data: OrderInfoDatum?
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Datum
|
||||
struct OrderInfoDatum: Codable ,Identifiable {
|
||||
var id = UUID().uuidString
|
||||
let inviteUserID: String?
|
||||
let planID: Int?
|
||||
let siteID, couponID: String?
|
||||
let type: Int
|
||||
let period: String
|
||||
let couponCode: String?
|
||||
let tradeNo: String
|
||||
let callbackNo: String?
|
||||
let totalAmount: Double
|
||||
let handlingAmount, discountAmount, surplusAmount, refundAmount: Double?
|
||||
let balanceAmount, surplusOrderIDS,paymentID: Int?
|
||||
let status: Int
|
||||
// let actualCommissionBalance, paidAt: String?
|
||||
let createdAt, updatedAt: Int
|
||||
let commissionStatus, commissionBalance: Int?
|
||||
let tixianstatus: String?
|
||||
let plan: PlanOrder
|
||||
|
||||
var status_zh: String {
|
||||
switch status {
|
||||
case 0:
|
||||
return "待支付"
|
||||
case 1:
|
||||
return "已支付"
|
||||
case 2:
|
||||
return "已取消"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
var period_zh: String {
|
||||
switch period {
|
||||
case "month_price":
|
||||
return "月付"
|
||||
case "quarter_price":
|
||||
return "季付"
|
||||
case "half_year_price":
|
||||
return "半年付"
|
||||
case "year_price":
|
||||
return "年付"
|
||||
default:
|
||||
return "月付"
|
||||
}
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case inviteUserID = "invite_user_id"
|
||||
case planID = "plan_id"
|
||||
case siteID = "site_id"
|
||||
case couponID = "coupon_id"
|
||||
case paymentID = "payment_id"
|
||||
case type, period
|
||||
case couponCode = "coupon_code"
|
||||
case tradeNo = "trade_no"
|
||||
case callbackNo = "callback_no"
|
||||
case totalAmount = "total_amount"
|
||||
case handlingAmount = "handling_amount"
|
||||
case discountAmount = "discount_amount"
|
||||
case surplusAmount = "surplus_amount"
|
||||
case refundAmount = "refund_amount"
|
||||
case balanceAmount = "balance_amount"
|
||||
case surplusOrderIDS = "surplus_order_ids"
|
||||
case status
|
||||
case commissionStatus = "commission_status"
|
||||
case commissionBalance = "commission_balance"
|
||||
// case actualCommissionBalance = "actual_commission_balance"
|
||||
// case paidAt = "paid_at"
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
case tixianstatus, plan
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Plan
|
||||
struct PlanOrder: Codable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let speedLimit: Int?
|
||||
let show, sort, renew, groupID, transferEnable: Int?
|
||||
let content: String
|
||||
let monthPrice, quarterPrice, halfYearPrice, yearPrice: Int?
|
||||
let twoYearPrice, threeYearPrice, onetimePrice, resetPrice: Double?
|
||||
let resetTrafficMethod: Int?
|
||||
let capacityLimit: Int?
|
||||
let createdAt, updatedAt: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case groupID = "group_id"
|
||||
case transferEnable = "transfer_enable"
|
||||
case name
|
||||
case speedLimit = "speed_limit"
|
||||
case show, sort, renew, content
|
||||
case monthPrice = "month_price"
|
||||
case quarterPrice = "quarter_price"
|
||||
case halfYearPrice = "half_year_price"
|
||||
case yearPrice = "year_price"
|
||||
case twoYearPrice = "two_year_price"
|
||||
case threeYearPrice = "three_year_price"
|
||||
case onetimePrice = "onetime_price"
|
||||
case resetPrice = "reset_price"
|
||||
case resetTrafficMethod = "reset_traffic_method"
|
||||
case capacityLimit = "capacity_limit"
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// PaymentReponse.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - PaymentReponse
|
||||
struct PaymentReponse: Codable {
|
||||
let status, message: String?
|
||||
let data: [PaymentReponseDatum]?
|
||||
}
|
||||
|
||||
// MARK: - Datum
|
||||
struct PaymentReponseDatum: Codable {
|
||||
let id: Int
|
||||
let name, payment: String
|
||||
let icon,handlingFeePercent,handlingFeeFixed: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, name, payment, icon
|
||||
case handlingFeeFixed = "handling_fee_fixed"
|
||||
case handlingFeePercent = "handling_fee_percent"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// PlanResponse.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/12.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
|
||||
// MARK: - Welcome
|
||||
struct PlanResponse: Codable {
|
||||
let data: [DatuPlanResponse]?
|
||||
let message: String?
|
||||
}
|
||||
|
||||
// MARK: - Datum
|
||||
struct DatuPlanResponse: Codable,Identifiable {
|
||||
var id = UUID()
|
||||
let idOLD, groupID, transferEnable: Int?
|
||||
let name: String
|
||||
let deviceLimit: Int?
|
||||
let show: Int?
|
||||
let renew: Int?
|
||||
let content: String?
|
||||
let monthPrice, quarterPrice, halfYearPrice, yearPrice: Int?
|
||||
let onetimePrice: Int?
|
||||
let createdAt, updatedAt: Int?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case idOLD = "id"
|
||||
case groupID = "group_id"
|
||||
case transferEnable = "transfer_enable"
|
||||
case name
|
||||
case deviceLimit = "device_limit"
|
||||
case show, renew, content
|
||||
case monthPrice = "month_price"
|
||||
case quarterPrice = "quarter_price"
|
||||
case halfYearPrice = "half_year_price"
|
||||
case yearPrice = "year_price"
|
||||
case onetimePrice = "onetime_price"
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Server.swift
|
||||
// Server
|
||||
//
|
||||
// Created by zues on 12/09/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// Sample Server and data...
|
||||
|
||||
//struct Server: Identifiable {
|
||||
//
|
||||
// var id = UUID().uuidString
|
||||
// var name: String
|
||||
// var flag: String
|
||||
//}
|
||||
|
||||
//var servers = [
|
||||
// Server(name: "Auto Select", flag: "us"),
|
||||
// Server(name: "United States", flag: "us"),
|
||||
// Server(name: "India", flag: "in"),
|
||||
// Server(name: "United Kingdom", flag: "uk"),
|
||||
// Server(name: "France", flag: "fr"),
|
||||
// Server(name: "Germany", flag: "ge"),
|
||||
// Server(name: "Singapore", flag: "si"),
|
||||
//]
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// File.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/12.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Welcome
|
||||
struct SubscribeReponse: Codable {
|
||||
let data: SubscribeReponseClass?
|
||||
let message: String?
|
||||
}
|
||||
|
||||
// MARK: - DataClass
|
||||
struct SubscribeReponseClass: Codable {
|
||||
let planID: Int?
|
||||
let token: String
|
||||
|
||||
let u, d, transferEnable: Int
|
||||
|
||||
let email, uuid: String
|
||||
let plan: Plan?
|
||||
let subscribeURL: String
|
||||
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case planID = "plan_id"
|
||||
case token
|
||||
case u, d
|
||||
case transferEnable = "transfer_enable"
|
||||
case email, uuid, plan
|
||||
case subscribeURL = "subscribe_url"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Plan
|
||||
struct Plan: Codable {
|
||||
let id, groupID: Int
|
||||
let name: String
|
||||
|
||||
let show,renew: Int
|
||||
|
||||
let content: String?
|
||||
let monthPrice, quarterPrice, halfYearPrice, yearPrice: Int?
|
||||
let onetimePrice: Int?
|
||||
let createdAt, updatedAt: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case groupID = "group_id"
|
||||
|
||||
case name
|
||||
|
||||
case show, renew, content
|
||||
case monthPrice = "month_price"
|
||||
case quarterPrice = "quarter_price"
|
||||
case halfYearPrice = "half_year_price"
|
||||
case yearPrice = "year_price"
|
||||
|
||||
case onetimePrice = "onetime_price"
|
||||
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// TicketChatReponse.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - TicketChatReponse
|
||||
struct TicketChatReponse: Codable {
|
||||
let status, message: String?
|
||||
let data: TicketChatReponseDataClass?
|
||||
}
|
||||
|
||||
// MARK: - DataClass
|
||||
struct TicketChatReponseDataClass: Codable {
|
||||
|
||||
|
||||
let id, level, replyStatus, status: Int
|
||||
let subject: String
|
||||
let message: [Message]
|
||||
let createdAt, updatedAt, userID: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case level
|
||||
case replyStatus = "reply_status"
|
||||
case status, subject, message
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
case userID = "user_id"
|
||||
}
|
||||
}
|
||||
|
||||
struct Message: Codable ,Identifiable,Equatable {
|
||||
var id = UUID().uuidString
|
||||
let idold, ticketID: Int
|
||||
let isMe: Bool
|
||||
let message: String
|
||||
let photo:String?
|
||||
let createdAt, updatedAt: Int
|
||||
let profilePic : String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case idold = "id"
|
||||
case ticketID = "ticket_id"
|
||||
case isMe = "is_me"
|
||||
case message
|
||||
case photo
|
||||
case profilePic
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// TicketsReponse.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
// MARK: - Welcome
|
||||
struct TicketsReponse: Codable {
|
||||
let status, message: String?
|
||||
let data: [TicketsReponseDatum]?
|
||||
}
|
||||
|
||||
//Message
|
||||
// MARK: - Datum
|
||||
struct TicketsReponseDatum: Codable,Identifiable {
|
||||
var id = UUID().uuidString
|
||||
let idOLD, level, replyStatus, status: Int
|
||||
let subject: String
|
||||
let message: String?
|
||||
let createdAt, updatedAt, userID: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case idOLD = "id"
|
||||
case level
|
||||
case replyStatus = "reply_status"
|
||||
case status, subject, message
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
case userID = "user_id"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// UserInfo.swift
|
||||
// LoginKit
|
||||
//
|
||||
// Created by Mac on 2024/10/12.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
struct UserInfo: Codable {
|
||||
struct Data: Codable {
|
||||
let email: String
|
||||
let transfer_enable: Int
|
||||
let device_limit: Int?
|
||||
let last_login_at: Int
|
||||
let created_at: Int
|
||||
let banned: Int
|
||||
let remind_expire: Int
|
||||
let remind_traffic: Int
|
||||
let expired_at: Int?
|
||||
let balance: Int
|
||||
let commission_balance: Int
|
||||
let plan_id: Int
|
||||
let discount: Int?
|
||||
let commission_rate: Int?
|
||||
let telegram_id: Int?
|
||||
let uuid: String
|
||||
let avatar_url: String
|
||||
}
|
||||
let data: Data
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
//
|
||||
// File.swift
|
||||
// LoginKit
|
||||
//
|
||||
// Created by Mac on 2024/10/12.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Foundation
|
||||
|
||||
|
||||
class UserManager {
|
||||
static let shared = UserManager()
|
||||
|
||||
public let configURL = "https://api.gooapis.com/api/yaya/"
|
||||
|
||||
public let appversion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
|
||||
|
||||
// UserDefaults keys
|
||||
private let loginStatusKey = "isLoggedIn"
|
||||
private let userEmailKey = "userEmail"
|
||||
private let avatarurlKey = "avatar_url"
|
||||
private let autodataKey = "autodata"
|
||||
private let subURLDataKey = "subURLData"
|
||||
private let baseURLKey = "baseURLKey"
|
||||
private let crisptoken = "crisptoken"
|
||||
|
||||
/// UserDefaults实例
|
||||
private let defaults = UserDefaults.standard
|
||||
|
||||
|
||||
func crispTokenData()-> String{
|
||||
return defaults.string(forKey: crisptoken) ?? ""
|
||||
}
|
||||
|
||||
func baseURL()-> String{
|
||||
return defaults.string(forKey: "baseURLKey") ?? ""
|
||||
}
|
||||
|
||||
func baseDYURL()-> String{
|
||||
defaults.string(forKey: "baseDYURL") ?? ""
|
||||
}
|
||||
|
||||
func mainregisterURL()-> String{
|
||||
defaults.string(forKey: "mainregisterURLKey") ?? ""
|
||||
}
|
||||
func paymentURL()-> String{
|
||||
defaults.string(forKey: "paymentURLKey") ?? ""
|
||||
}
|
||||
func telegramUrl()-> String{
|
||||
defaults.string(forKey: "telegramUrlKey") ?? ""
|
||||
}
|
||||
func kefuUrl()-> String{
|
||||
defaults.string(forKey: "kefuUrlKey") ?? ""
|
||||
}
|
||||
func websiteURL()-> String{
|
||||
defaults.string(forKey: "websiteURLKey") ?? ""
|
||||
}
|
||||
|
||||
|
||||
|
||||
func storeCrispTokenData (data: String) {
|
||||
defaults.set(data, forKey: crisptoken)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
/// 存储基础URL
|
||||
func storebaseURLData(data: String) {
|
||||
defaults.set(data, forKey: "baseURLKey")
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
func storemainregisterURLData(data: String) {
|
||||
defaults.set(data, forKey: "mainregisterURLKey")
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
func storepaymentURLData(data: String) {
|
||||
defaults.set(data, forKey: "paymentURLKey")
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
func storetelegramUrlData(data: String) {
|
||||
defaults.set(data, forKey: "telegramUrlKey")
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
func storekefuUrlData(data: String) {
|
||||
defaults.set(data, forKey: "kefuUrlKey")
|
||||
defaults.synchronize()
|
||||
}
|
||||
func storewebsiteURLData(data: String) {
|
||||
defaults.set(data, forKey: "websiteURLKey")
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
func storebaseDYURL(data:String){
|
||||
defaults.setValue(data, forKey: "baseDYURL")
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// 更新登录状态
|
||||
func updateLoginStatus(_ loggedIn: Bool) {
|
||||
defaults.set(loggedIn, forKey: loginStatusKey)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
func saveUserInfoToLocal(userInfo: UserInfo) {
|
||||
do {
|
||||
let data = try JSONEncoder().encode(userInfo)
|
||||
defaults.set(data, forKey: "userInfo")
|
||||
defaults.synchronize()
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查登录状态
|
||||
func isUserLoggedIn() -> Bool {
|
||||
return defaults.bool(forKey: loginStatusKey)
|
||||
}
|
||||
|
||||
/// 存储用户信息
|
||||
func storeUserInfo(email: String, avator: String) {
|
||||
defaults.set(email, forKey: userEmailKey)
|
||||
defaults.set(avator, forKey: avatarurlKey)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
/// 获取用户信息
|
||||
func getUserInfo() -> (email: String, avator: String) {
|
||||
let email = defaults.string(forKey: userEmailKey) ?? ""
|
||||
let avator = defaults.string(forKey: avatarurlKey) ?? ""
|
||||
return (email, avator)
|
||||
}
|
||||
|
||||
/// 存储认证数据
|
||||
func storeAutoData(data: String) {
|
||||
defaults.set(data, forKey: autodataKey)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
/// 获取订阅URL数据
|
||||
func getSuburlData() -> String {
|
||||
return defaults.string(forKey: subURLDataKey) ?? ""
|
||||
}
|
||||
|
||||
/// 存储订阅URL数据
|
||||
func storeSuburlData(data: String) {
|
||||
defaults.set(data, forKey: subURLDataKey)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
/// 获取认证数据
|
||||
func getAutoData() -> String {
|
||||
return defaults.string(forKey: autodataKey) ?? ""
|
||||
}
|
||||
|
||||
/// 清除用户数据
|
||||
func clearUserData() {
|
||||
defaults.removeObject(forKey: loginStatusKey)
|
||||
defaults.removeObject(forKey: userEmailKey)
|
||||
defaults.removeObject(forKey: avatarurlKey)
|
||||
defaults.removeObject(forKey: autodataKey)
|
||||
defaults.removeObject(forKey: subURLDataKey)
|
||||
|
||||
defaults.removeObject(forKey: "goGrouptag")
|
||||
defaults.removeObject(forKey: "selectedNode")
|
||||
defaults.removeObject(forKey: "paymentURLKey")
|
||||
defaults.removeObject(forKey: "baseDYURL")
|
||||
defaults.removeObject(forKey: "serverData")
|
||||
defaults.removeObject(forKey: "lastFetchTime")
|
||||
|
||||
let dictionary = defaults.dictionaryRepresentation()
|
||||
dictionary.keys.forEach { key in
|
||||
defaults.removeObject(forKey: key)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
//
|
||||
// ActiveDashboardViewNewUI.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Zeus on 2024/10/11.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import Foundation
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
import ApplicationLibrary
|
||||
|
||||
@MainActor
|
||||
public struct ActiveDashboardViewNewUI: View {
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@Environment(\.selection) private var parentSelection
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
@EnvironmentObject private var profile: ExtensionProfile
|
||||
@State private var isLoading = true
|
||||
@State private var profileList: [ProfilePreview] = []
|
||||
@State private var selectedProfileID: Int64 = 0
|
||||
@State private var alert: Alert?
|
||||
@State private var selection = DashboardPage.overview
|
||||
@State private var systemProxyAvailable = false
|
||||
@State private var systemProxyEnabled = false
|
||||
|
||||
@State private var alerthandleXufei = false
|
||||
@State private var tixingdingyueEnabled = false
|
||||
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
if isLoading {
|
||||
ProgressView().onAppear {
|
||||
Task {
|
||||
await doReload()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ApplicationLibrary.inPreview {
|
||||
body1
|
||||
} else {
|
||||
body1
|
||||
.onAppear {
|
||||
Task {
|
||||
await doReloadSystemProxy()
|
||||
}
|
||||
}
|
||||
.onChangeCompat(of: profile.status) { newStatus in
|
||||
if newStatus == .connected {
|
||||
Task {
|
||||
await doReloadSystemProxy()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var body1: some View {
|
||||
VStack {
|
||||
#if os(iOS) || os(tvOS)
|
||||
if ApplicationLibrary.inPreview || profile.status.isConnectedStrict {
|
||||
/* Picker("Page", selection: $selection) {
|
||||
ForEach(DashboardPage.allCases) { page in
|
||||
page.label
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
#if os(iOS)
|
||||
.padding([.leading, .trailing])
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
TabView(selection: $selection) {
|
||||
ForEach(DashboardPage.allCases) { page in
|
||||
page.contentView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
|
||||
.tag(page)
|
||||
}
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .always))
|
||||
*/
|
||||
OverviewView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
|
||||
} else {
|
||||
if tixingdingyueEnabled {
|
||||
XufeiTixingButton {
|
||||
alerthandleXufei = true
|
||||
}.alert(Text("续费提醒"), isPresented: $alerthandleXufei) {
|
||||
Button("续费"){
|
||||
environments.opentixingSubnodes.send()
|
||||
}
|
||||
} message: {
|
||||
Text("您的账户已经过期,请续费后继续体验畅快感受;如果您刚购买完请耐心等待,会员时长会在1分钟内到账。")
|
||||
}
|
||||
|
||||
}else{
|
||||
OverviewView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
|
||||
}
|
||||
|
||||
}
|
||||
#elseif os(macOS)
|
||||
OverviewView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
|
||||
#endif
|
||||
}.onReceive(environments.updateTixingdingyueEnabled, perform: { _ in
|
||||
tixingdingyueEnabled = false
|
||||
})
|
||||
.onReceive(environments.profileUpdate) { _ in
|
||||
|
||||
Task {
|
||||
await doReload()
|
||||
}
|
||||
}
|
||||
.onReceive(environments.selectedProfileUpdate) { _ in
|
||||
Task {
|
||||
selectedProfileID = await SharedPreferences.selectedProfileID.get()
|
||||
if profile.status.isConnected {
|
||||
await doReloadSystemProxy()
|
||||
}
|
||||
}
|
||||
}
|
||||
.alertBinding($alert)
|
||||
}
|
||||
|
||||
|
||||
|
||||
private func doReload() async {
|
||||
defer {
|
||||
isLoading = false
|
||||
}
|
||||
if ApplicationLibrary.inPreview {
|
||||
profileList = [
|
||||
ProfilePreview(Profile(id: 0, name: "profile local", type: .local, path: "")),
|
||||
ProfilePreview(Profile(id: 1, name: "profile remote", type: .remote, path: "", lastUpdated: Date(timeIntervalSince1970: 0))),
|
||||
]
|
||||
systemProxyAvailable = true
|
||||
systemProxyEnabled = true
|
||||
selectedProfileID = 0
|
||||
|
||||
} else {
|
||||
do {
|
||||
profileList = try await ProfileManager.list().map { ProfilePreview($0) }
|
||||
if profileList.isEmpty {
|
||||
print("profileList.isEmpty 是否提醒续费\(tixingdingyueEnabled), \(UserManager.shared.getSuburlData().count)")
|
||||
//add profile remote
|
||||
if tixingdingyueEnabled {
|
||||
return
|
||||
}
|
||||
Task {
|
||||
if UserManager.shared.getSuburlData().count > 5 {
|
||||
await createProfile()
|
||||
}
|
||||
}
|
||||
|
||||
await environments.reload()
|
||||
|
||||
return
|
||||
}
|
||||
selectedProfileID = await SharedPreferences.selectedProfileID.get()
|
||||
if profileList.filter({ profile in
|
||||
profile.id == selectedProfileID
|
||||
})
|
||||
.isEmpty {
|
||||
selectedProfileID = profileList[0].id
|
||||
await SharedPreferences.selectedProfileID.set(selectedProfileID)
|
||||
}
|
||||
|
||||
|
||||
// try await ProfileManager.list().forEach { p in
|
||||
// await ProfileManager.update(p)
|
||||
// }
|
||||
|
||||
|
||||
} catch {
|
||||
alert = Alert(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
environments.emptyProfiles = profileList.isEmpty
|
||||
}
|
||||
|
||||
private nonisolated func doReloadSystemProxy() async {
|
||||
do {
|
||||
let status = try LibboxNewStandaloneCommandClient()!.getSystemProxyStatus()
|
||||
await MainActor.run {
|
||||
systemProxyAvailable = status.available
|
||||
systemProxyEnabled = status.enabled
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func createProfile() async {
|
||||
do {
|
||||
try await createProfileBackground()
|
||||
} catch {
|
||||
|
||||
print("createProfile : \(error)")
|
||||
}
|
||||
environments.profileUpdate.send()
|
||||
}
|
||||
|
||||
private nonisolated func createProfileBackground() async throws {
|
||||
print("正在创建 profile")
|
||||
|
||||
let nextProfileID = try await ProfileManager.nextID()
|
||||
|
||||
var savePath = ""
|
||||
let remoteURL: String = UserManager.shared.getSuburlData()
|
||||
var lastUpdated: Date? = nil
|
||||
var remoteContent = try HTTPClient().getString(remoteURL)
|
||||
print("第一次订阅URL的内容 remoteContent: \(remoteContent.count)")
|
||||
if(remoteContent.count < 3 && UserManager.shared.paymentURL().count < 3){
|
||||
//TODO: 苹果审核(当当前用户没有续费时,随便注册账号,且在苹果审核的时候注册安装后,直接给默认流量地址)
|
||||
//节点数据为空,那么直接去订阅一个默认小流量的节点位置,免费的
|
||||
remoteContent = try HTTPClient().getString(UserManager.shared.baseDYURL())
|
||||
print("请求服务器的议定 remoteContent: \(remoteContent.count)")
|
||||
|
||||
}else{
|
||||
if(remoteContent.count < 3){
|
||||
|
||||
//提醒续费,正常用户
|
||||
print("提醒续费,正常用户")
|
||||
await MainActor.run {
|
||||
tixingdingyueEnabled = true
|
||||
}
|
||||
}else{
|
||||
print("正常有订阅的付费客户")
|
||||
//正常有订阅的付费客户
|
||||
await MainActor.run {
|
||||
tixingdingyueEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if remoteContent.isEmpty {
|
||||
return
|
||||
}
|
||||
var error: NSError?
|
||||
LibboxCheckConfig(remoteContent, &error)
|
||||
if let error {
|
||||
|
||||
throw error
|
||||
|
||||
}
|
||||
let profileConfigDirectory = FilePath.sharedDirectory.appendingPathComponent("configs", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: profileConfigDirectory, withIntermediateDirectories: true)
|
||||
let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json")
|
||||
try remoteContent.write(to: profileConfig, atomically: true, encoding: .utf8)
|
||||
savePath = profileConfig.relativePath
|
||||
|
||||
lastUpdated = .now
|
||||
|
||||
try await ProfileManager.create(Profile(
|
||||
name: "VPN",
|
||||
type: ProfileType.remote,
|
||||
path: savePath,
|
||||
remoteURL: remoteURL,
|
||||
autoUpdate: true,
|
||||
autoUpdateInterval: 60,
|
||||
lastUpdated: lastUpdated
|
||||
))
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
try UIProfileUpdateTask.configure()
|
||||
let list = try await ProfileManager.list()
|
||||
list.forEach { profileOne in
|
||||
print("createProfileBackground 查询到所有订阅节点:{ \n \(profileOne.id ?? 0 ) \(profileOne.path) \n \(profileOne.remoteURL ?? "") }")
|
||||
}
|
||||
await MainActor.run {
|
||||
environments.openProfileGetSuccess.send()
|
||||
}
|
||||
#else
|
||||
try await ProfileUpdateTask.configure()
|
||||
#endif
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//
|
||||
// BgView.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/12.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
//
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// DashboardViewNewUI.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Zeus on 2024/10/11.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
import ApplicationLibrary
|
||||
|
||||
|
||||
|
||||
struct DashboardViewNewUI: View {
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
@EnvironmentObject private var profile: ExtensionProfile
|
||||
@State private var alert: Alert?
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ActiveDashboardViewNewUI()
|
||||
}
|
||||
.alertBinding($alert)
|
||||
.onChangeCompat(of: profile.status) { newValue in
|
||||
if newValue == .disconnecting || newValue == .connected {
|
||||
Task {
|
||||
await checkServiceError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated func checkServiceError() async {
|
||||
var error: NSError?
|
||||
let message = LibboxReadServiceError(&error)
|
||||
if error != nil {
|
||||
return
|
||||
}
|
||||
await MainActor.run {
|
||||
alert = Alert(title: Text("VPN 服务异常"), message: Text(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//
|
||||
// GroupListViewModel.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/19.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,126 @@
|
||||
//
|
||||
// QuestionView.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/24.
|
||||
//
|
||||
|
||||
import ApplicationLibrary
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct QuestionView: View {
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode // 环境变量用于控制视图的呈现
|
||||
|
||||
var body: some View {
|
||||
NavigationView(content: {
|
||||
VStack{
|
||||
ZStack{
|
||||
|
||||
HStack{
|
||||
Button(action: {
|
||||
|
||||
presentationMode.wrappedValue.dismiss() // 手动触发返回
|
||||
}, label: {
|
||||
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("返回")
|
||||
.foregroundColor(.white)
|
||||
}).padding(10) // 增加可点击区域
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
|
||||
Text("问题解答")
|
||||
.fontWeight(.bold).lineLimit(1)
|
||||
}
|
||||
.padding()
|
||||
// .padding(.top,edges.top)
|
||||
.foregroundColor(.white)
|
||||
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack {
|
||||
|
||||
HStack{
|
||||
Text("App软件无法使用问题?") // 让 Text 宽度为屏幕宽度
|
||||
Spacer()
|
||||
}
|
||||
Section("") {
|
||||
Text("1:确认网络没问题,包括 WIFI 和 移动网络可用 \n2:确认手机可访问浏览器正常数据,可以尝试访问 https://www.apple.com 试试网络是否连接正常。")
|
||||
.buttonStyle(.plain).font(.subheadline)
|
||||
.foregroundColor(Color.white.opacity(0.8))
|
||||
|
||||
}
|
||||
Divider() // 分隔线
|
||||
|
||||
|
||||
HStack{
|
||||
Text("初次连接提示创建“VPN连接”失败?") // 让 Text 宽度为屏幕宽度
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Section("") {
|
||||
Text("到这种情况可能是VPN功能被系统被禁止了。可以尝试到设置中开启“VPN连接”权限,若还有问题请随时联系客服。")
|
||||
.buttonStyle(.plain).font(.subheadline)
|
||||
.foregroundColor(Color.white.opacity(0.8))
|
||||
|
||||
}
|
||||
Divider() // 分隔线
|
||||
|
||||
HStack{
|
||||
Text("指定的国家连接不上?") // 让 Text 宽度为屏幕宽度
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Section("") {
|
||||
Text("快连每个国家的背后都有成干上百的网络节点在进行智能调控,若遇到指定国家连接不上的情况请随时联系客服为您定位原因,请放心一定能解决!")
|
||||
.buttonStyle(.plain).font(.subheadline)
|
||||
.foregroundColor(Color.white.opacity(0.8))
|
||||
|
||||
}
|
||||
Divider() // 分隔线
|
||||
|
||||
|
||||
HStack{
|
||||
Text("连接成功了,但无法访问外网?") // 让 Text 宽度为屏幕宽度
|
||||
Spacer()
|
||||
}
|
||||
Section("") {
|
||||
Text("恭喜您遇到了严重的软件BUG,请尽快联系客服复现问题,领取BUG奖励~!!!")
|
||||
.buttonStyle(.plain).font(.subheadline).foregroundColor(Color.white.opacity(0.8))
|
||||
|
||||
|
||||
}
|
||||
|
||||
Divider() // 分隔线
|
||||
|
||||
HStack{
|
||||
Text("遇到VPN无法连接怎么办?") // 让 Text 宽度为屏幕宽度
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Section("") {
|
||||
Text("1:确定您所安装的UUVPN是最新版本 2:尝试重启客户端 3:联系我们的客服 ").multilineTextAlignment(.leading)
|
||||
.buttonStyle(.plain).font(.subheadline).foregroundColor(Color.white.opacity(0.8))
|
||||
|
||||
|
||||
}
|
||||
}.padding(.leading,10).padding(.trailing,10)
|
||||
}.foregroundColor(.white)
|
||||
}.navigationBarHidden(true).edgesIgnoringSafeArea(.bottom).background(
|
||||
BackgroundBg()
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
.navigationBarHidden(true)
|
||||
.navigationBarBackButtonHidden(true) // 隐藏返回按钮
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
//
|
||||
// InviteListView.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import ApplicationLibrary
|
||||
|
||||
//InviteListView
|
||||
struct InviteListView : View{
|
||||
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
@State private var codeslist: [InviteCode] = []
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
@State private var alert: Alert?
|
||||
@State private var stat: [Int]?
|
||||
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode // 环境变量用于控制视图的呈现
|
||||
|
||||
var body: some View {
|
||||
NavigationView(content: {
|
||||
VStack {
|
||||
|
||||
// Navigation Bar
|
||||
ZStack{
|
||||
|
||||
|
||||
HStack{
|
||||
|
||||
Button(action: {
|
||||
isPresented = false
|
||||
presentationMode.wrappedValue.dismiss() // 手动触发返回
|
||||
}, label: {
|
||||
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("返回")
|
||||
.foregroundColor(.white)
|
||||
}).padding(10) // 增加可点击区域
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
Task{
|
||||
await genoneInvite()
|
||||
}
|
||||
}, label: {
|
||||
Text("生成邀请码").foregroundColor(.white)
|
||||
})
|
||||
|
||||
}
|
||||
Spacer()
|
||||
VStack(spacing: 5){
|
||||
|
||||
Text("我的邀请")
|
||||
.fontWeight(.bold).lineLimit(1).frame(width: UIScreen.main.bounds.width*0.5)
|
||||
|
||||
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
|
||||
|
||||
}
|
||||
.padding(.all)
|
||||
|
||||
|
||||
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
.padding()
|
||||
}
|
||||
|
||||
if let errorMessage = errorMessage {
|
||||
Text(errorMessage)
|
||||
.bold()
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center) // 确保多行文本居中
|
||||
.padding()
|
||||
}
|
||||
|
||||
//
|
||||
// // Subscription Plans
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 16) {
|
||||
VStack {
|
||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), alignment: .leading) {
|
||||
if let st = stat {
|
||||
StatusItem("邀请注册人数") {
|
||||
|
||||
StatusLine("人数", "\(st[0])")
|
||||
}
|
||||
StatusItem("邀请佣金比例") {
|
||||
|
||||
StatusLine("比例", "\(st[3])%")
|
||||
}
|
||||
StatusItem("确认中的佣金") {
|
||||
|
||||
StatusLine("金额(¥)", "\(st[1])")
|
||||
}
|
||||
StatusItem("累计获得佣金") {
|
||||
|
||||
StatusLine("金额(¥)", "\(st[2])")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}.background {
|
||||
GeometryReader { geometry in
|
||||
Rectangle()
|
||||
.fill(.clear)
|
||||
.frame(height: 1)
|
||||
.onChangeCompat(of: geometry.size.width) { newValue in
|
||||
//updateColumnCount(newValue)
|
||||
}
|
||||
.onAppear {
|
||||
// updateColumnCount(geometry.size.width)
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
.frame(alignment: .topLeading)
|
||||
.padding([.top, .leading, .trailing])
|
||||
|
||||
if codeslist.isEmpty {
|
||||
// Text("Loading plans...") // Loading indicator
|
||||
if !isLoading
|
||||
{
|
||||
Text("暂无记录").font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.multilineTextAlignment(.center) // 确保多行文本居中
|
||||
.padding()
|
||||
}
|
||||
} else {
|
||||
|
||||
ForEach(codeslist) { plan in
|
||||
Button(action: {
|
||||
UIPasteboard.general.string = UserManager.shared.mainregisterURL()+plan.code
|
||||
alert = Alert(okMessage: "复制成功: " + UserManager.shared.mainregisterURL()+plan.code )
|
||||
}, label: {
|
||||
InviteItemView(invicode: plan)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}.padding(.top, 20)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Subscription Notice
|
||||
Text(" ")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
.background(
|
||||
LinearGradient(colors: [
|
||||
|
||||
Color("BG1"),
|
||||
Color("BG1"),
|
||||
Color("BG2"),
|
||||
Color("BG2"),
|
||||
|
||||
], startPoint: .top, endPoint: .bottom)
|
||||
)
|
||||
.navigationBarHidden(true)
|
||||
// .background(.white)
|
||||
.onAppear(){
|
||||
Task{
|
||||
await getCodesList()
|
||||
}
|
||||
}.edgesIgnoringSafeArea(.bottom)
|
||||
|
||||
})
|
||||
.navigationBarHidden(true)
|
||||
.navigationBarBackButtonHidden(true) // 隐藏返回按钮
|
||||
.alertBinding($alert)
|
||||
|
||||
}
|
||||
|
||||
public func genoneInvite() async {
|
||||
|
||||
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/invite/save")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
alert = Alert(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
struct InviteReponse2: Codable {
|
||||
let status, message: String?
|
||||
}
|
||||
// Parse the user info response
|
||||
if let response = try? JSONDecoder().decode(InviteReponse2.self, from: data),let message = response.message {
|
||||
|
||||
if let status = response.status, status == "success" {
|
||||
alert = Alert(okMessage: message){
|
||||
|
||||
Task{
|
||||
await getCodesList()
|
||||
}
|
||||
}
|
||||
}else{
|
||||
alert = Alert(errorMessage: message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
|
||||
private struct StatusLine: View {
|
||||
private let name: String
|
||||
private let value: String
|
||||
|
||||
init(_ name: String, _ value: String) {
|
||||
self.name = name
|
||||
self.value = value
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(name)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text(value)
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct StatusItem<T>: View where T: View {
|
||||
private let title: String
|
||||
@ViewBuilder private let content: () -> T
|
||||
|
||||
init(_ title: String, @ViewBuilder content: @escaping () -> T) {
|
||||
self.title = title
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text(title)
|
||||
.font(.subheadline)
|
||||
Spacer()
|
||||
}.padding(.bottom, 8)
|
||||
content()
|
||||
}
|
||||
.frame(minWidth: 125, alignment: .topLeading)
|
||||
#if os(tvOS)
|
||||
.padding(EdgeInsets(top: 20, leading: 26, bottom: 20, trailing: 26))
|
||||
#else
|
||||
.padding(EdgeInsets(top: 10, leading: 13, bottom: 10, trailing: 13))
|
||||
#endif
|
||||
.background(backgroundColor.opacity(0.3))
|
||||
.cornerRadius(10)
|
||||
}
|
||||
|
||||
private var backgroundColor: Color {
|
||||
#if os(iOS)
|
||||
return Color(.gray)
|
||||
#elseif os(macOS)
|
||||
return Color(nsColor: .textBackgroundColor)
|
||||
#elseif os(tvOS)
|
||||
return Color(uiColor: .black)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func getCodesList() async {
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/invite/fetch")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
|
||||
if let error = error {
|
||||
self.errorMessage = "数据请求失败: \(error.localizedDescription)"
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
self.errorMessage = "数据请求失败"
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
// if let jsonString = String(data: data, encoding: .utf8) {
|
||||
// print("Response data: \(jsonString)")
|
||||
// } else {
|
||||
// print("Failed to convert data to string.")
|
||||
// }
|
||||
//
|
||||
|
||||
// Parse the user info response
|
||||
if let Subscribe = try? JSONDecoder().decode(InviteReponse.self, from: data) {
|
||||
|
||||
if let codes = Subscribe.data?.codes {
|
||||
codeslist = codes
|
||||
stat = Subscribe.data?.stat
|
||||
}else{
|
||||
self.errorMessage = Subscribe.message ?? ""
|
||||
}
|
||||
|
||||
}else{
|
||||
self.errorMessage = "数据请求失败: JSON 解析错误"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct InviteItemView: View {
|
||||
var invicode : InviteCode
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
|
||||
Text("邀请码:" + invicode.code )
|
||||
.font(.headline)
|
||||
.foregroundColor(.black).multilineTextAlignment(.leading)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
Text("创建时间:"+TimestampConverter.convertTimestampToDateString(invicode.createdAt))
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
.padding(2)
|
||||
.frame(maxWidth: .infinity)
|
||||
.foregroundColor(.gray)
|
||||
// .offset(x: -16, y:16)
|
||||
.offset(y:10)
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(10)
|
||||
.shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 3)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
//
|
||||
// InviteView.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/12.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct InviteView: View {
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
@State private var inviteCode: String = "" // This can be dynamically updated.
|
||||
@State private var alert: Alert?
|
||||
|
||||
|
||||
var body: some View {
|
||||
ZStack(){
|
||||
|
||||
LinearGradient(colors: [
|
||||
Color("BG1"),
|
||||
Color("BG1"),
|
||||
Color("BG2"),
|
||||
Color("BG2"),
|
||||
], startPoint: .top, endPoint: .bottom)
|
||||
|
||||
VStack(spacing: 20) {
|
||||
Spacer().frame(height: 30)
|
||||
// Invitation code section
|
||||
HStack {
|
||||
Text("我的邀请码:")
|
||||
.font(.headline)
|
||||
.foregroundColor(.black)
|
||||
Text(inviteCode)
|
||||
.font(.headline)
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
|
||||
// Illustration image
|
||||
// Image("panda_invite") // Add your asset with this name
|
||||
LottieView(animationFileName: "7a4ce4b4" , loopMode: .loop)
|
||||
.aspectRatio(contentMode: .fill)
|
||||
//// .frame(width: getRect().width,height: getRect().width)
|
||||
.scaleEffect(0.3)
|
||||
.scaledToFit()
|
||||
.frame(width: 200, height: 150)
|
||||
|
||||
// Invite description
|
||||
VStack(spacing: 5) {
|
||||
Text("邀请您的朋友成为 VIP,邀请好友享首次付费30%返佣,赶紧邀请好友一同使用开心上网!")
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.headline)
|
||||
.padding(10)
|
||||
.foregroundColor(.black)
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Copy invite code button
|
||||
Button(action: {
|
||||
// Copy invite code action
|
||||
if inviteCode.count <= 1 {
|
||||
Task {
|
||||
await reloadInvite(copy: true)
|
||||
}
|
||||
}else{
|
||||
UIPasteboard.general.string = UserManager.shared.mainregisterURL()+inviteCode
|
||||
alert = Alert(okMessage: "复制成功")
|
||||
}
|
||||
}) {
|
||||
Text("复制邀请链接")
|
||||
.font(.system(size: 16))
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(LinearGradient(gradient: Gradient(colors: [Color.green.opacity(0.7), Color.green]), startPoint: .leading, endPoint: .trailing))
|
||||
.cornerRadius(25)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.horizontal, 40)
|
||||
|
||||
Spacer().frame(height: 30)
|
||||
|
||||
}
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(Color.white)
|
||||
.shadow(radius: 10)
|
||||
|
||||
)
|
||||
|
||||
.padding()
|
||||
|
||||
}
|
||||
.onAppear(perform: {
|
||||
Task {
|
||||
await reloadInvite(copy: false)
|
||||
}
|
||||
})
|
||||
.alertBinding($alert)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public func reloadInvite(copy: Bool) async {
|
||||
|
||||
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/invite/fetch")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
alert = Alert(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the user info response
|
||||
if let invites = try? JSONDecoder().decode(InviteReponse.self, from: data) {
|
||||
if let invite = invites.data?.codes?.first?.code {
|
||||
self.inviteCode = invite
|
||||
if copy {
|
||||
UIPasteboard.general.string = UserManager.shared.mainregisterURL()+inviteCode
|
||||
alert = Alert(okMessage: "复制成功")
|
||||
}
|
||||
}else{
|
||||
//生成第一条
|
||||
Task{
|
||||
await genoneInvite()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}else{
|
||||
alert = Alert(errorMessage: "访问失败")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
public func genoneInvite() async {
|
||||
|
||||
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/invite/save")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
alert = Alert(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
return
|
||||
}
|
||||
// Parse the user info response
|
||||
if let _ = try? JSONDecoder().decode(InviteReponse.self, from: data) {
|
||||
Task{
|
||||
await reloadInvite(copy: false)
|
||||
}
|
||||
|
||||
// if invites.status == "success" {}
|
||||
/**
|
||||
|
||||
{
|
||||
"status": "success",
|
||||
"message": "\u64cd\u4f5c\u6210\u529f",
|
||||
"data": true,
|
||||
"error": null
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
//
|
||||
// ForgotPassword.swift
|
||||
// LoginKit
|
||||
//
|
||||
// Created by zues on 04/08/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ForgotPassword: View {
|
||||
@Binding var showResetView: Bool
|
||||
/// View Properties
|
||||
//@State private var emailID: String = ""
|
||||
@Binding var emailID: String
|
||||
/// Environment Properties
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var askOTP: Bool = false
|
||||
//@State private var otpText: String = ""
|
||||
|
||||
@Binding var otpText: String
|
||||
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 15, content: {
|
||||
/// Back Button
|
||||
Button(action: {
|
||||
dismiss()
|
||||
}, label: {
|
||||
Image(systemName: "arrow.left")
|
||||
.font(.title2)
|
||||
.foregroundStyle(.gray)
|
||||
})
|
||||
.padding(.top, 10)
|
||||
|
||||
Text("忘记密码?")
|
||||
.font(.largeTitle)
|
||||
//.fontWeight(.heavy)
|
||||
.padding(.top, 5)
|
||||
|
||||
Text("请输入您的电子邮件,以便我们可以发送验证码。")
|
||||
.font(.caption)
|
||||
//.fontWeight(.semibold)
|
||||
.foregroundStyle(.gray)
|
||||
.padding(.top, -5)
|
||||
|
||||
|
||||
|
||||
VStack(spacing: 25) {
|
||||
/// Custom Text Fields
|
||||
CustomTF(sfIcon: "at", hint: "输入邮箱", value: $emailID)
|
||||
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
|
||||
if let errorMessage = errorMessage {
|
||||
Text(errorMessage)
|
||||
.bold()
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center) // 确保多行文本居中
|
||||
.padding()
|
||||
}
|
||||
|
||||
/// SignUp Button
|
||||
GradientButton(title: "发送验证码", icon: "arrow.right") {
|
||||
/// YOUR CODE
|
||||
/// After the Link sent
|
||||
Task {
|
||||
await sendemail(email:emailID)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.hSpacing(.trailing)
|
||||
/// Disabling Until the Data is Entered
|
||||
.disableWithOpacity(emailID.isEmpty || isLoading)
|
||||
}
|
||||
.padding(.top, 20)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
})/// OTP Prompt
|
||||
.sheet(isPresented: $askOTP, onDismiss: {
|
||||
/// YOUR CODE
|
||||
/// Reset OTP if You Want
|
||||
// otpText = ""
|
||||
}, content: {
|
||||
if #available(iOS 16.4, *) {
|
||||
/// Since I wanted a Custom Sheet Corner Radius
|
||||
OTPView(otpText: $otpText,onButtonClick: {
|
||||
print(otpText)
|
||||
Task {
|
||||
dismiss()
|
||||
// try? await Task.sleep(for: .seconds(0))
|
||||
/// Showing the Reset View
|
||||
showResetView = true
|
||||
}
|
||||
})
|
||||
.presentationDetents([.height(350)])
|
||||
.presentationCornerRadius(30)
|
||||
} else {
|
||||
if #available(iOS 16.0, *) {
|
||||
OTPView(otpText: $otpText,onButtonClick: {
|
||||
print(otpText)
|
||||
Task {
|
||||
dismiss()
|
||||
// try? await Task.sleep(for: .seconds(0))
|
||||
/// Showing the Reset View
|
||||
showResetView = true
|
||||
}
|
||||
}).presentationDetents([.height(350)])
|
||||
} else {
|
||||
OTPView(otpText: $otpText,onButtonClick: {
|
||||
print(otpText)
|
||||
Task {
|
||||
dismiss()
|
||||
// try? await Task.sleep(for: .seconds(0))
|
||||
/// Showing the Reset View
|
||||
showResetView = true
|
||||
}
|
||||
})
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
}
|
||||
})
|
||||
.padding(.vertical, 15)
|
||||
.padding(.horizontal, 25)
|
||||
/// Since this is going to be a Sheet.
|
||||
.interactiveDismissDisabled()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@MainActor
|
||||
public func sendemail(email: String) async {
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())passport/comm/sendEmailVerify?email=\(email)&recaptcha_data=")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
|
||||
self.isLoading = false
|
||||
|
||||
if let error = error {
|
||||
self.errorMessage = "验证码发送失败: \(error.localizedDescription)"
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
self.errorMessage = "验证码发送失败:数据返回空"
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the user info response
|
||||
if let message = try? JSONDecoder().decode(MessageResponse.self, from: data) {
|
||||
dump(message)
|
||||
if let _ = message.errors{
|
||||
self.errorMessage = message.message
|
||||
}else{
|
||||
askOTP.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,693 @@
|
||||
//
|
||||
// Login.swift
|
||||
// LoginKit
|
||||
//
|
||||
// Created by zues on 04/08/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import Crisp
|
||||
|
||||
// MARK: - Errors
|
||||
struct Errors: Codable {
|
||||
let email: [String]?
|
||||
}
|
||||
|
||||
|
||||
struct LoginResponseSuccess: Codable {
|
||||
let data: LoginResponseSuccessClass?
|
||||
let message: String?
|
||||
let status: String?
|
||||
let errors: Errors?
|
||||
}
|
||||
|
||||
// MARK: - DataClass
|
||||
struct LoginResponseSuccessClass: Codable {
|
||||
let token: String
|
||||
let isAdmin: Int?
|
||||
let authData: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case token
|
||||
case isAdmin = "is_admin"
|
||||
case authData = "auth_data"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct Login: View {
|
||||
|
||||
@Binding var isLoggedIn: Bool
|
||||
|
||||
@Binding var showSignup: Bool
|
||||
|
||||
/// View Properties
|
||||
@State private var emailID: String = ""
|
||||
|
||||
@State private var password: String = ""
|
||||
@State private var showForgotPasswordView: Bool = false
|
||||
/// Reset Password View (with New Password and Confimration Password View)
|
||||
@State private var showResetView: Bool = false
|
||||
/// Optional, Present If you want to ask OTP for login
|
||||
@State private var askOTP: Bool = false
|
||||
@State private var otpText: String = ""
|
||||
|
||||
@State private var showKefuView: Bool = false
|
||||
|
||||
|
||||
@State private var userInfo: UserInfo? = nil
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
@State private var alert: Alert?
|
||||
@State private var okMessage: String?
|
||||
|
||||
@FocusState private var focusedField: Bool
|
||||
|
||||
@State private var isresetpwdLoading = false
|
||||
@State private var resetpwdErrorMessage = ""
|
||||
|
||||
@AppStorage("isFirstOpen") private var isFirstOpen: Bool?
|
||||
|
||||
@State var onboardingItems: [OnboardingItem] = [
|
||||
.init(title: "最稳定的加速器",
|
||||
subTitle: "順暢瀏覽社交媒體,同時支持Netflix及Youtube等流媒體內容平台,4K清晰度不卡頓。",
|
||||
lottieView: .init(name: "12c853b3",bundle: .main)),
|
||||
.init(title: "永不跑路的VPN加速器",
|
||||
subTitle: "一鍵訪問匿名互聯網,告別繁瑣的配置,跨平臺設備支持,隨時隨地連接網絡。",
|
||||
lottieView: .init(name: "2c630b55",bundle: .main)),
|
||||
.init(title: "加密访问,全球畅连",
|
||||
subTitle: "隱藏您的IP地址。互聯網流量通過TLS加密保護免受任何黑客探測、監控和攻擊。",
|
||||
lottieView: .init(name: "5a519db7",bundle: .main))
|
||||
]
|
||||
// MARK: Current Slide Index
|
||||
@State var currentIndex: Int = 0
|
||||
|
||||
var onBoardView: some View {
|
||||
|
||||
GeometryReader{
|
||||
let size = $0.size
|
||||
VStack(spacing: 40, content: {
|
||||
Spacer().frame(height: 10)
|
||||
|
||||
HStack(spacing: 0){
|
||||
ForEach($onboardingItems) { $item in
|
||||
let isLastSlide = (currentIndex == onboardingItems.count - 1)
|
||||
VStack{
|
||||
// MARK: Top Nav Bar
|
||||
HStack{
|
||||
Button(action: {
|
||||
if currentIndex > 0{
|
||||
currentIndex -= 1
|
||||
playAnimation()
|
||||
}
|
||||
}, label: {
|
||||
Image(systemName: "arrow.backward")
|
||||
}).tint(Color("Main"))
|
||||
.opacity(currentIndex > 0 ? 1 : 0)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
// Button("跳过"){
|
||||
// currentIndex = onboardingItems.count - 1
|
||||
// playAnimation()
|
||||
// }
|
||||
// .opacity(isLastSlide ? 0 : 1)
|
||||
}
|
||||
.animation(.easeInOut, value: currentIndex)
|
||||
.tint(Color("Main"))
|
||||
//.fontWeight(.bold)
|
||||
|
||||
// MARK: Movable Slides
|
||||
VStack(spacing: 15){
|
||||
let offset = -CGFloat(currentIndex) * size.width
|
||||
// MARK: Resizable Lottie View
|
||||
ResizableLottieView(onboardingItem: $item)
|
||||
.frame(height: size.width*0.8)
|
||||
.onAppear {
|
||||
// MARK: Intially Playing First Slide Animation
|
||||
if currentIndex == indexOf(item){
|
||||
item.lottieView.play(toProgress: 0.7)
|
||||
}
|
||||
}
|
||||
.offset(x: offset)
|
||||
.animation(.easeInOut(duration: 0.5), value: currentIndex)
|
||||
|
||||
Text(item.title)
|
||||
.font(.title.bold())
|
||||
.offset(x: offset)
|
||||
.animation(.easeInOut(duration: 0.5).delay(0.1), value: currentIndex)
|
||||
|
||||
Text(item.subTitle)
|
||||
.font(.system(size: 14))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal,15)
|
||||
.foregroundColor(.gray)
|
||||
.offset(x: offset)
|
||||
.animation(.easeInOut(duration: 0.5).delay(0.2), value: currentIndex)
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
// MARK: Next / Login Button
|
||||
VStack(spacing: 15){
|
||||
Button(isLastSlide ? "登录" : "下一步"){
|
||||
if currentIndex < onboardingItems.count - 1{
|
||||
// MARK: Pausing Previous Animation
|
||||
let currentProgress = onboardingItems[currentIndex].lottieView.currentProgress
|
||||
onboardingItems[currentIndex].lottieView.currentProgress = (currentProgress == 0 ? 0.7 : currentProgress)
|
||||
currentIndex += 1
|
||||
// MARK: Playing Next Animation from Start
|
||||
playAnimation()
|
||||
}
|
||||
if isLastSlide {
|
||||
withAnimation(){
|
||||
isFirstOpen = false
|
||||
}
|
||||
}
|
||||
}
|
||||
// .fontWeight(.bold)
|
||||
.foregroundColor(.white)
|
||||
.padding(.vertical,isLastSlide ? 13 : 12)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background {
|
||||
Capsule()
|
||||
.fill(Color("Main2"))
|
||||
}
|
||||
.padding(.horizontal,isLastSlide ? 50 : 100)
|
||||
|
||||
|
||||
if currentIndex == onboardingItems.count - 1{
|
||||
|
||||
HStack{
|
||||
// Button("Terms of Service"){}
|
||||
|
||||
// Button("Privacy Policy"){}
|
||||
}
|
||||
.font(.caption2)
|
||||
//.underline(true, color: .primary)
|
||||
.offset(y: 5)
|
||||
}
|
||||
}.onTapGesture {
|
||||
if currentIndex < onboardingItems.count - 1{
|
||||
// MARK: Pausing Previous Animation
|
||||
let currentProgress = onboardingItems[currentIndex].lottieView.currentProgress
|
||||
onboardingItems[currentIndex].lottieView.currentProgress = (currentProgress == 0 ? 0.7 : currentProgress)
|
||||
currentIndex += 1
|
||||
// MARK: Playing Next Animation from Start
|
||||
playAnimation()
|
||||
}
|
||||
if isLastSlide {
|
||||
withAnimation(){
|
||||
isFirstOpen = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.animation(.easeInOut, value: isLastSlide)
|
||||
.padding(15)
|
||||
.frame(width: size.width, height: size.height)
|
||||
}
|
||||
}
|
||||
.frame(width: size.width * CGFloat(onboardingItems.count),alignment: .leading)
|
||||
}).ignoresSafeArea(.all)
|
||||
|
||||
// if !#available(iOS 15.0, *) {
|
||||
//
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
func playAnimation(){
|
||||
onboardingItems[currentIndex].lottieView.currentProgress = 0
|
||||
onboardingItems[currentIndex].lottieView.play(toProgress: 0.7)
|
||||
}
|
||||
|
||||
// MARK: Retreving Index of the Item in the Array
|
||||
func indexOf(_ item: OnboardingItem)->Int{
|
||||
if let index = onboardingItems.firstIndex(of: item){
|
||||
return index
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if isFirstOpen ?? true {
|
||||
onBoardView
|
||||
}else{
|
||||
loginView
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var loginView: some View {
|
||||
VStack(alignment: .leading, spacing: 15, content: {
|
||||
Spacer(minLength: 0)
|
||||
|
||||
// HStack{
|
||||
// Spacer()
|
||||
// Image("applogo").resizable().frame(width: 100, height: 100).cornerRadius(20)
|
||||
// Spacer()
|
||||
// }
|
||||
|
||||
Text("登录")
|
||||
.font(.largeTitle)
|
||||
//.fontWeight(.heavy)
|
||||
|
||||
Text("请按登录按钮继续")
|
||||
.font(.callout)
|
||||
//.fontWeight(.semibold)
|
||||
.foregroundStyle(.gray)
|
||||
.padding(.top, -5)
|
||||
|
||||
VStack(spacing: 25) {
|
||||
/// Custom Text Fields
|
||||
CustomTF(sfIcon: "at", hint: "Email", value: $emailID)
|
||||
|
||||
CustomTF(sfIcon: "lock", hint: "密码", isPassword: true, value: $password)
|
||||
.padding(.top, 5).focused($focusedField)
|
||||
|
||||
|
||||
HStack(content: {
|
||||
|
||||
Button("忘记密码?") {
|
||||
showForgotPasswordView.toggle()
|
||||
}
|
||||
.font(.callout)
|
||||
//.fontWeight(.heavy)
|
||||
.tint(.white)
|
||||
|
||||
Button {
|
||||
showKefuView.toggle()
|
||||
} label: {
|
||||
|
||||
Text("联系客服") .font(.callout)
|
||||
Image(systemName: "person.crop.circle.badge.questionmark")
|
||||
}.tint(.white)
|
||||
})
|
||||
.hSpacing(.trailing)
|
||||
|
||||
|
||||
|
||||
/// Login Button
|
||||
///
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
|
||||
if let errorMessage = errorMessage {
|
||||
Text(errorMessage)
|
||||
.bold()
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center) // 确保多行文本居中
|
||||
.padding()
|
||||
}
|
||||
|
||||
if let okmsg = okMessage {
|
||||
Text(okmsg)
|
||||
.bold()
|
||||
.foregroundColor(Color("Main"))
|
||||
.multilineTextAlignment(.center) // 确保多行文本居中
|
||||
.padding()
|
||||
}
|
||||
|
||||
if let userInfo = userInfo {
|
||||
Text("Logged in as: \(userInfo.data.email)")
|
||||
// AsyncImage(url: URL(string: userInfo.data.avatar_url))
|
||||
// .frame(width: 64, height: 64)
|
||||
// .clipShape(Circle())
|
||||
}
|
||||
|
||||
GradientButton(title: "登录", icon: "arrow.right") {
|
||||
/// YOUR CODE
|
||||
//askOTP.toggle()
|
||||
Task{
|
||||
await loginUser()
|
||||
}
|
||||
}
|
||||
.hSpacing(.trailing)
|
||||
/// Disabling Until the Data is Entered
|
||||
.disableWithOpacity(emailID.isEmpty || password.isEmpty || isLoading)
|
||||
}
|
||||
.padding(.top, 20)
|
||||
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
HStack(spacing: 6) {
|
||||
Text("没有账号?")
|
||||
.foregroundStyle(.gray)
|
||||
|
||||
Button("注册") {
|
||||
showSignup.toggle()
|
||||
}
|
||||
//.fontWeight(.bold)
|
||||
.tint(.white)
|
||||
}
|
||||
.font(.callout)
|
||||
.hSpacing()
|
||||
})
|
||||
.onAppear(){
|
||||
//获取 config 信息
|
||||
Task{
|
||||
await getConfig()
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 15)
|
||||
.padding(.horizontal, 25)
|
||||
//.toolbar(.hidden, for: .navigationBar)
|
||||
/// Asking Email ID For Sending Reset Link
|
||||
.sheet(isPresented: $showForgotPasswordView, content: {
|
||||
if #available(iOS 16.4, *) {
|
||||
/// Since I wanted a Custom Sheet Corner Radius
|
||||
ForgotPassword(showResetView: $showResetView, emailID: $emailID, otpText: $otpText)
|
||||
.presentationDetents([.height(300)])
|
||||
.presentationCornerRadius(30)
|
||||
} else {
|
||||
ForgotPassword(showResetView: $showResetView, emailID: $emailID, otpText: $otpText)
|
||||
// .presentationDetents([.height(300)])
|
||||
}
|
||||
})
|
||||
.fullScreenCover(isPresented: $showKefuView, content: {
|
||||
SupportView()
|
||||
})
|
||||
/// Resetting New Password
|
||||
.sheet(isPresented: $showResetView, content: {
|
||||
if #available(iOS 16.4, *) {
|
||||
/// Since I wanted a Custom Sheet Corner Radius
|
||||
|
||||
PasswordResetView(isresetpwdLoading: $isresetpwdLoading, resetpwdErrorMessage: $resetpwdErrorMessage) { password in
|
||||
print(otpText + " " + password)
|
||||
|
||||
Task{
|
||||
await resetpwd(password:password)
|
||||
}
|
||||
}
|
||||
.presentationDetents([.height(350)])
|
||||
.presentationCornerRadius(30)
|
||||
} else {
|
||||
PasswordResetView(isresetpwdLoading: $isresetpwdLoading, resetpwdErrorMessage: $resetpwdErrorMessage){password in
|
||||
print(otpText + " " + password)
|
||||
|
||||
Task{
|
||||
await resetpwd(password:password)
|
||||
}
|
||||
}
|
||||
// .presentationDetents([.height(350)])
|
||||
}
|
||||
})
|
||||
.alertBinding($alert)
|
||||
/// OTP Prompt
|
||||
.sheet(isPresented: $askOTP, onDismiss: {
|
||||
/// YOUR CODE
|
||||
/// Reset OTP if You Want
|
||||
// otpText = ""
|
||||
}, content: {
|
||||
if #available(iOS 16.4, *) {
|
||||
/// Since I wanted a Custom Sheet Corner Radius
|
||||
OTPView(otpText: $otpText)
|
||||
.presentationDetents([.height(350)])
|
||||
.presentationCornerRadius(30)
|
||||
} else {
|
||||
OTPView(otpText: $otpText)
|
||||
//.presentationDetents([.height(350)])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func resetpwd(password:String) async {
|
||||
isresetpwdLoading = true
|
||||
let loginUrl = URL(string: "\(UserManager.shared.baseURL())passport/auth/forget?email=\(emailID)&password=\(password)&email_code=\(otpText)")!
|
||||
var request = URLRequest(url: loginUrl)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
|
||||
print(loginUrl)
|
||||
let loginData = ["email": emailID, "password": password, "email_code": otpText]
|
||||
guard let httpBody = try? JSONSerialization.data(withJSONObject: loginData) else {
|
||||
self.resetpwdErrorMessage = "请求格式错误"
|
||||
self.isresetpwdLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
request.httpBody = httpBody
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
isresetpwdLoading = false
|
||||
if let error = error {
|
||||
resetpwdErrorMessage = ( "密码重置失败: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
resetpwdErrorMessage = ("密码重置失败: 数据为空")
|
||||
return
|
||||
}
|
||||
|
||||
struct resetpwdResponseSuccess: Codable {
|
||||
let message: String?
|
||||
let status: String?
|
||||
}
|
||||
|
||||
// Parse the login response and get the authorization token
|
||||
if let jsonResponse = try? JSONDecoder().decode(resetpwdResponseSuccess.self, from: data){
|
||||
dump(jsonResponse)
|
||||
|
||||
if let status = jsonResponse.status , status == "success" {
|
||||
|
||||
withAnimation {
|
||||
showResetView = false
|
||||
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
|
||||
alert = Alert(okMessage: "密码重置成功!")
|
||||
}
|
||||
}else{
|
||||
if let message = jsonResponse.message {
|
||||
resetpwdErrorMessage = ( "\(message)")
|
||||
}else{
|
||||
resetpwdErrorMessage = ( "密码重置失败: 未知错误")
|
||||
}
|
||||
}
|
||||
}else{
|
||||
resetpwdErrorMessage = ("密码重置失败: 数据 json 格式错误")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
task.resume()
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Networking Logic
|
||||
|
||||
func getConfig() async {
|
||||
|
||||
let loginUrl = URL(string: UserManager.shared.configURL)!
|
||||
var request = URLRequest(url: loginUrl)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(Bundle.main.bundleIdentifier ?? "", forHTTPHeaderField: "bid")
|
||||
request.addValue(UserManager.shared.appversion, forHTTPHeaderField: "appver")
|
||||
request.addValue(UIDevice.current.model, forHTTPHeaderField: "model")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
if let _ = error {
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
|
||||
Task{
|
||||
await getConfig()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
|
||||
Task{
|
||||
await getConfig()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Parse the login response and get the authorization token
|
||||
struct configReponse: Codable {
|
||||
let baseURL, mainregisterURL,paymentURL,crisptoken: String
|
||||
let telegramurl, kefuurl, websiteURL,baseDYURL: String
|
||||
let message: String
|
||||
let code: Int
|
||||
}
|
||||
|
||||
if let jsonString = String(data: data, encoding: .utf8) {
|
||||
print("Response data: \(jsonString)")
|
||||
}
|
||||
|
||||
if let jsonResponse = try? JSONDecoder().decode(configReponse.self, from: data){
|
||||
// dump(jsonResponse)
|
||||
if jsonResponse.code == 1 {
|
||||
//save data
|
||||
UserManager.shared.storebaseURLData(data: jsonResponse.baseURL)
|
||||
UserManager.shared.storemainregisterURLData(data: jsonResponse.mainregisterURL)
|
||||
UserManager.shared.storepaymentURLData(data: jsonResponse.paymentURL)
|
||||
UserManager.shared.storetelegramUrlData(data: jsonResponse.telegramurl)
|
||||
UserManager.shared.storekefuUrlData(data: jsonResponse.kefuurl)
|
||||
UserManager.shared.storewebsiteURLData(data: jsonResponse.websiteURL)
|
||||
UserManager.shared.storebaseDYURL(data: jsonResponse.baseDYURL)
|
||||
//crisptoken
|
||||
CrispSDK.configure(websiteID: jsonResponse.crisptoken)
|
||||
}
|
||||
}else{
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
|
||||
Task{
|
||||
await getConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
|
||||
func loginUser() async {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
focusedField = false
|
||||
let loginUrl = URL(string: "\(UserManager.shared.baseURL())passport/auth/login")!
|
||||
var request = URLRequest(url: loginUrl)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let loginData = ["email": emailID, "password": password, "captchaData": ""]
|
||||
guard let httpBody = try? JSONSerialization.data(withJSONObject: loginData) else {
|
||||
self.errorMessage = "Invalid login data"
|
||||
self.isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
request.httpBody = httpBody
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
|
||||
if let error = error {
|
||||
self.errorMessage = "登录失败: \(error.localizedDescription)"
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
self.errorMessage = "登录失败: 数据为空"
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Parse the login response and get the authorization token
|
||||
if let jsonResponse = try? JSONDecoder().decode(LoginResponseSuccess.self, from: data){
|
||||
dump(jsonResponse)
|
||||
|
||||
if let _ = jsonResponse.errors, let message = jsonResponse.message{
|
||||
// Proceed to fetch user info
|
||||
// fetchUserInfo(token: token)
|
||||
self.errorMessage = "\(message)"
|
||||
}else{
|
||||
if let authData = jsonResponse.data?.authData {
|
||||
|
||||
UserManager.shared.updateLoginStatus(true)
|
||||
UserManager.shared.storeAutoData(data: authData)
|
||||
self.okMessage = "登录成功,正在跳转..."
|
||||
self.isLoading = true
|
||||
|
||||
|
||||
UserManager.shared.storeUserInfo(email: emailID, avator: "")
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
|
||||
self.isLoggedIn = true
|
||||
}
|
||||
//去掉记录
|
||||
// Task{
|
||||
// await fetchUserInfo(token: authData)
|
||||
// }
|
||||
}else{
|
||||
if let message = jsonResponse.message {
|
||||
self.errorMessage = "\(message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}else{
|
||||
self.errorMessage = "登录失败: 数据 json 格式错误"
|
||||
}
|
||||
//{"data":{"token":"880e8785746c0bf72b2e01a882a678e7","is_admin":1,"auth_data":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6Miwic2Vzc2lvbiI6Ijc4MzE1Yjc5YmNlNTliYWIzOTg5MTIzMTFhNDkwN2NiIn0.00fdwJ85bOSycfnkOPmhF7pSR1VV9WczBEeE9aXncQQ"}}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func fetchUserInfo(token: String) async {
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/info")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue("\(token)", forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
self.errorMessage = "Failed to get user info: \(error.localizedDescription)"
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
self.errorMessage = "No user data received"
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Parse the user info response
|
||||
if let userInfo = try? JSONDecoder().decode(UserInfo.self, from: data) {
|
||||
self.userInfo = userInfo
|
||||
// Save user info to local storage
|
||||
|
||||
|
||||
UserManager.shared.saveUserInfoToLocal(userInfo: userInfo)
|
||||
UserManager.shared.storeUserInfo(email: userInfo.data.email, avator: userInfo.data.avatar_url)
|
||||
|
||||
|
||||
// 读取登录状态和用户信息
|
||||
let isLoggedIn = UserManager.shared.isUserLoggedIn()
|
||||
let userInfo = UserManager.shared.getUserInfo()
|
||||
let autoData = UserManager.shared.getAutoData()
|
||||
|
||||
print("Logged in: \(isLoggedIn)")
|
||||
print("Email: \(userInfo.email), avator: \(userInfo.avator)")
|
||||
print("Auto Data: \(autoData)")
|
||||
self.isLoggedIn = true
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
// Save user info to local storage (UserDefaults)
|
||||
|
||||
/**
|
||||
{"data":{"email":"ceshi1@qq.com","transfer_enable":119185342464,"device_limit":null,"last_login_at":1725893596,"created_at":1725893596,"banned":0,"remind_expire":1,"remind_traffic":1,"expired_at":null,"balance":0,"commission_balance":0,"plan_id":1,"discount":null,"commission_rate":null,"telegram_id":null,"uuid":"0b0d55e3-9f6f-4ee0-9a73-2901655cfaf2","avatar_url":"https:\/\/cravatar.cn\/avatar\/845735bc7a6186ae5bad56e7cb87d88b?s=64&d=identicon"}}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// LoginKit
|
||||
//
|
||||
// Created by zues on 04/08/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LoginContentView: View {
|
||||
|
||||
@Binding var isLoggedIn: Bool
|
||||
|
||||
/// View Properties
|
||||
@State private var showSignup: Bool = false
|
||||
/// Keyboard Status
|
||||
@State private var isKeyboardShowing: Bool = false
|
||||
var body: some View {
|
||||
NavigationView{
|
||||
if showSignup {
|
||||
// 如果 showSignup 为 true,则显示 SignUp 界面
|
||||
SignUp(isLoggedIn: $isLoggedIn,showSignup: $showSignup).onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification), perform: { _ in
|
||||
/// Disabling it for signup view
|
||||
if !showSignup {
|
||||
isKeyboardShowing = true
|
||||
}
|
||||
})
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification), perform: { _ in
|
||||
isKeyboardShowing = false
|
||||
}).background(BackgroundBg())
|
||||
} else {
|
||||
/// Checking if any Keyboard is Visible
|
||||
// 如果 showSignup 为 false,则显示 Login 界面
|
||||
Login(isLoggedIn: $isLoggedIn, showSignup: $showSignup).onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification), perform: { _ in
|
||||
/// Disabling it for signup view
|
||||
if !showSignup {
|
||||
isKeyboardShowing = true
|
||||
}
|
||||
})
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification), perform: { _ in
|
||||
isKeyboardShowing = false
|
||||
}).background(BackgroundBg())
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.preferredColorScheme(.dark)
|
||||
//
|
||||
// /// iOS 17 Bounce Animations
|
||||
// if #available(iOS 17, *) {
|
||||
// /// Since this Project Supports iOS 16 too.
|
||||
// CircleView()
|
||||
// .animation(.smooth(duration: 0.45, extraBounce: 0), value: showSignup)
|
||||
// .animation(.smooth(duration: 0.45, extraBounce: 0), value: isKeyboardShowing)
|
||||
// } else {
|
||||
// CircleView()
|
||||
// .animation(.easeInOut(duration: 0.3), value: showSignup)
|
||||
// .animation(.easeInOut(duration: 0.3), value: isKeyboardShowing)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
/// Moving Blurred background
|
||||
@ViewBuilder
|
||||
func CircleView() -> some View {
|
||||
Circle()
|
||||
.fill(.linearGradient(colors: [.main, .main2], startPoint: .top, endPoint: .bottom))
|
||||
.frame(width: 200, height: 200)
|
||||
/// Moving When the Signup Pages Loads/Dismisses
|
||||
.offset(x: showSignup ? 90 : -90, y: -90 - (isKeyboardShowing ? 200 : 0))
|
||||
.blur(radius: 15)
|
||||
.hSpacing(showSignup ? .trailing : .leading)
|
||||
.vSpacing(.top)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// OTPView.swift
|
||||
// LoginKit
|
||||
//
|
||||
// Created by zues on 04/08/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct OTPView: View {
|
||||
@Binding var otpText: String
|
||||
/// Environment Properties
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
var onButtonClick: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 15, content: {
|
||||
/// Back Button
|
||||
Button(action: {
|
||||
dismiss()
|
||||
}, label: {
|
||||
Image(systemName: "arrow.left")
|
||||
.font(.title2)
|
||||
.foregroundStyle(.gray)
|
||||
})
|
||||
.padding(.top, 15)
|
||||
|
||||
Text("输入验证码")
|
||||
.font(.largeTitle)
|
||||
//.fontWeight(.heavy)
|
||||
.padding(.top, 5)
|
||||
|
||||
Text("一个 6 位数的验证码已发送至您的电子邮件。")
|
||||
.font(.caption)
|
||||
//.fontWeight(.semibold)
|
||||
.foregroundStyle(.gray)
|
||||
.padding(.top, -5)
|
||||
|
||||
VStack(spacing: 25) {
|
||||
/// Custom OTP TextField
|
||||
OTPVerificationView(otpText: $otpText)
|
||||
|
||||
/// SignUp Button
|
||||
GradientButton(title: "下一步", icon: "arrow.right") {
|
||||
/// YOUR CODE
|
||||
onButtonClick?()
|
||||
}
|
||||
.hSpacing(.trailing)
|
||||
/// Disabling Until the Data is Entered
|
||||
.disableWithOpacity(otpText.isEmpty)
|
||||
}
|
||||
.padding(.top, 20)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
})
|
||||
.padding(.vertical, 15)
|
||||
.padding(.horizontal, 25)
|
||||
/// Since this is going to be a Sheet.
|
||||
.interactiveDismissDisabled()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// PasswordResetView.swift
|
||||
// LoginKit
|
||||
//
|
||||
// Created by zues on 04/08/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PasswordResetView: View {
|
||||
/// View Properties
|
||||
@State private var password: String = ""
|
||||
@State private var confirmPassword: String = ""
|
||||
|
||||
@Binding var isresetpwdLoading: Bool
|
||||
@Binding var resetpwdErrorMessage : String
|
||||
|
||||
var onButtonClick: ((String) -> Void)?
|
||||
/// Environment Properties
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 15, content: {
|
||||
/// Back Button
|
||||
Button(action: {
|
||||
dismiss()
|
||||
}, label: {
|
||||
Image(systemName: "xmark")
|
||||
.font(.title2)
|
||||
.foregroundStyle(.gray)
|
||||
})
|
||||
.padding(.top, 10)
|
||||
|
||||
Text("重置密码")
|
||||
.font(.largeTitle)
|
||||
//.fontWeight(.heavy)
|
||||
.padding(.top, 5)
|
||||
|
||||
VStack(spacing: 25) {
|
||||
/// Custom Text Fields
|
||||
CustomTF(sfIcon: "lock", hint: "密码", value: $password)
|
||||
|
||||
CustomTF(sfIcon: "lock", hint: "再次确认密码", value: $confirmPassword)
|
||||
.padding(.top, 5)
|
||||
if isresetpwdLoading {
|
||||
ProgressView()
|
||||
}
|
||||
|
||||
if !resetpwdErrorMessage.isEmpty {
|
||||
Text(resetpwdErrorMessage)
|
||||
.bold()
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center) // 确保多行文本居中
|
||||
.padding()
|
||||
}
|
||||
/// SignUp Button
|
||||
GradientButton(title: "重置密码", icon: "arrow.right") {
|
||||
/// YOUR CODE
|
||||
/// Reset Password
|
||||
onButtonClick?(password)
|
||||
}
|
||||
.hSpacing(.trailing)
|
||||
/// Disabling Until the Data is Entered
|
||||
.disableWithOpacity(password.isEmpty || confirmPassword.isEmpty || confirmPassword != password || isresetpwdLoading)
|
||||
}
|
||||
.padding(.top, 20)
|
||||
Spacer(minLength: 0)
|
||||
})
|
||||
.padding(.vertical, 15)
|
||||
.padding(.horizontal, 25)
|
||||
/// Since this is going to be a Sheet.
|
||||
.interactiveDismissDisabled()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
//
|
||||
// Signup.swift
|
||||
// LoginKit
|
||||
//
|
||||
// Created by zues on 04/08/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SignUp: View {
|
||||
|
||||
@Binding var isLoggedIn: Bool
|
||||
@Binding var showSignup: Bool
|
||||
|
||||
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
|
||||
/// View Properties
|
||||
@State private var emailID: String = ""
|
||||
@State private var fullName: String = ""
|
||||
@State private var password: String = ""
|
||||
/// Optional, Present If you want to ask OTP for Signup
|
||||
@State private var askOTP: Bool = false
|
||||
@State private var otpText: String = ""
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 15, content: {
|
||||
|
||||
|
||||
// HStack{
|
||||
// Spacer()
|
||||
// Image("applogo").resizable().frame(width: 100, height: 100).cornerRadius(20)
|
||||
// Spacer()
|
||||
// }
|
||||
|
||||
|
||||
Text("注册")
|
||||
.font(.largeTitle)
|
||||
//.fontWeight(.heavy)
|
||||
.padding(.top, 25)
|
||||
|
||||
Text("请按注册按钮继续")
|
||||
.font(.callout)
|
||||
//.fontWeight(.semibold)
|
||||
.foregroundStyle(.gray)
|
||||
.padding(.top, -5)
|
||||
|
||||
VStack(spacing: 25) {
|
||||
/// Custom Text Fields
|
||||
CustomTF(sfIcon: "at", hint: "Email", value: $emailID)
|
||||
|
||||
CustomTF(sfIcon: "lock", hint: "密码", isPassword: true, value: $password)
|
||||
.padding(.top, 5)
|
||||
|
||||
CustomTF(sfIcon: "person", hint: "邀请人(可不填)", value: $fullName)
|
||||
.padding(.top, 5)
|
||||
|
||||
|
||||
Text("通过注册,你同意我们的[条款和条件](https://minipanda.soccertt.com/teams.html)和[隐私政策](https://minipanda.soccertt.com/privacy.html)")
|
||||
.font(.caption)
|
||||
.tint(.white)
|
||||
.foregroundStyle(.gray)
|
||||
.frame(height: 50)
|
||||
|
||||
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
|
||||
if let errorMessage = errorMessage {
|
||||
Text(errorMessage)
|
||||
.bold()
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center) // 确保多行文本居中
|
||||
.padding()
|
||||
}
|
||||
|
||||
|
||||
/// SignUp Button
|
||||
GradientButton(title: "注册", icon: "arrow.right") {
|
||||
/// YOUR CODE
|
||||
//askOTP.toggle()
|
||||
Task{
|
||||
await logupUser()
|
||||
}
|
||||
}
|
||||
.hSpacing(.trailing)
|
||||
/// Disabling Until the Data is Entered || fullName.isEmpty
|
||||
.disableWithOpacity(emailID.isEmpty || password.isEmpty || isLoading)
|
||||
}
|
||||
.padding(.top, 20)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
HStack(spacing: 6) {
|
||||
Text("已经有账号?")
|
||||
.foregroundStyle(.gray)
|
||||
|
||||
Button("登录") {
|
||||
showSignup = false
|
||||
}
|
||||
//.fontWeight(.bold)
|
||||
.tint(.white)
|
||||
}
|
||||
.font(.callout)
|
||||
.hSpacing()
|
||||
|
||||
})
|
||||
.padding(.vertical, 15)
|
||||
.padding(.horizontal, 25)
|
||||
//.toolbar(.hidden, for: .navigationBar)
|
||||
/// OTP Prompt
|
||||
.sheet(isPresented: $askOTP, onDismiss: {
|
||||
/// YOUR CODE
|
||||
/// Reset OTP if You Want
|
||||
// otpText = ""
|
||||
}, content: {
|
||||
if #available(iOS 16.4, *) {
|
||||
/// Since I wanted a Custom Sheet Corner Radius
|
||||
OTPView(otpText: $otpText)
|
||||
.presentationDetents([.height(350)])
|
||||
.presentationCornerRadius(30)
|
||||
} else {
|
||||
OTPView(otpText: $otpText)
|
||||
// .presentationDetents([.height(350)])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Networking Logic
|
||||
|
||||
func logupUser() async {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
let loginUrl = URL(string: "\(UserManager.shared.baseURL())passport/auth/register")!
|
||||
var request = URLRequest(url: loginUrl)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let loginData = ["email": emailID, "password": password, "captchaData": "","email_code":"","invite_code":fullName]
|
||||
guard let httpBody = try? JSONSerialization.data(withJSONObject: loginData) else {
|
||||
self.errorMessage = "Invalid login data"
|
||||
self.isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
request.httpBody = httpBody
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
|
||||
if let error = error {
|
||||
self.errorMessage = "注册失败: \(error.localizedDescription)"
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
self.errorMessage = "注册失败:数据返回空"
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the login response and get the authorization token
|
||||
|
||||
|
||||
if let jsonResponse = try? JSONDecoder().decode(LoginResponseSuccess.self, from: data){
|
||||
|
||||
|
||||
if let authData = jsonResponse.data?.authData {
|
||||
|
||||
UserManager.shared.updateLoginStatus(true)
|
||||
UserManager.shared.storeAutoData(data: authData)
|
||||
|
||||
|
||||
self.isLoggedIn = true
|
||||
}else{
|
||||
|
||||
if let message = jsonResponse.message {
|
||||
self.errorMessage = "\(message)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}else{
|
||||
self.errorMessage = "注册失败: 返回JSON格式错误"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***
|
||||
{
|
||||
"status": "success",
|
||||
"message": "\u64cd\u4f5c\u6210\u529f",
|
||||
"data": {
|
||||
"token": "56ffc6d0465212eca2856aa25dc1644e",
|
||||
"is_admin": null,
|
||||
"auth_data": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MTU0MDMsInNlc3Npb24iOiJiYzk3N2RhOTczN2RjZWVlM2FiNDc2NzAzMjBjOTI2OCJ9.WUPCzTlgsOMGM-DrsSMRrfBqgAB8GEysDyAofq6TgVo"
|
||||
},
|
||||
"error": null
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// MessageCenterView.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
//
|
||||
//
|
||||
// QuestionView.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/24.
|
||||
//
|
||||
|
||||
import ApplicationLibrary
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct MessageCenterView: View {
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
var body: some View {
|
||||
NavigationView(content: {
|
||||
VStack{
|
||||
ZStack{
|
||||
|
||||
HStack{
|
||||
Button(action: {
|
||||
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}, label: {
|
||||
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("返回")
|
||||
.foregroundColor(.white)
|
||||
}).padding(10)
|
||||
Spacer(minLength: 0)
|
||||
|
||||
}
|
||||
|
||||
Text("消息通知")
|
||||
.fontWeight(.bold).lineLimit(1)
|
||||
}
|
||||
.padding()
|
||||
// .padding(.top,edges.top)
|
||||
.foregroundColor(.white)
|
||||
|
||||
|
||||
Text("暂无消息").font(.subheadline).padding()
|
||||
Spacer()
|
||||
|
||||
}.navigationBarHidden(true).edgesIgnoringSafeArea(.bottom).foregroundColor(.white).background(
|
||||
LinearGradient(colors: [
|
||||
|
||||
Color("BG1"),
|
||||
Color("BG1"),
|
||||
Color("BG2"),
|
||||
Color("BG2"),
|
||||
|
||||
], startPoint: .top, endPoint: .bottom))
|
||||
|
||||
})
|
||||
|
||||
.navigationBarHidden(true)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
//
|
||||
// OnBoardingScreen.swift
|
||||
// AnimatedOnBoardingScreen
|
||||
//
|
||||
// Created by zues on 17/12/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Lottie
|
||||
|
||||
struct OnBoardingScreen: View {
|
||||
// MARK: OnBoarding Slides Model Data
|
||||
@State var onboardingItems: [OnboardingItem] = [
|
||||
]
|
||||
// MARK: Current Slide Index
|
||||
@State var currentIndex: Int = 0
|
||||
var body: some View {
|
||||
GeometryReader{
|
||||
let size = $0.size
|
||||
|
||||
HStack(spacing: 0){
|
||||
ForEach($onboardingItems) { $item in
|
||||
let isLastSlide = (currentIndex == onboardingItems.count - 1)
|
||||
VStack{
|
||||
// MARK: Top Nav Bar
|
||||
HStack{
|
||||
Button(action: {
|
||||
if currentIndex > 0{
|
||||
currentIndex -= 1
|
||||
playAnimation()
|
||||
}
|
||||
}, label: {
|
||||
Image(systemName: "arrow.backward")
|
||||
})
|
||||
.opacity(currentIndex > 0 ? 1 : 0)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
Button("跳过"){
|
||||
currentIndex = onboardingItems.count - 1
|
||||
playAnimation()
|
||||
}
|
||||
.opacity(isLastSlide ? 0 : 1)
|
||||
}
|
||||
.animation(.easeInOut, value: currentIndex)
|
||||
.tint(Color("Green"))
|
||||
//.fontWeight(.bold)
|
||||
|
||||
// MARK: Movable Slides
|
||||
VStack(spacing: 15){
|
||||
let offset = -CGFloat(currentIndex) * size.width
|
||||
// MARK: Resizable Lottie View
|
||||
ResizableLottieView(onboardingItem: $item)
|
||||
.frame(height: size.width)
|
||||
.onAppear {
|
||||
// MARK: Intially Playing First Slide Animation
|
||||
if currentIndex == indexOf(item){
|
||||
item.lottieView.play(toProgress: 0.7)
|
||||
}
|
||||
}
|
||||
.offset(x: offset)
|
||||
.animation(.easeInOut(duration: 0.5), value: currentIndex)
|
||||
|
||||
Text(item.title)
|
||||
.font(.title.bold())
|
||||
.offset(x: offset)
|
||||
.animation(.easeInOut(duration: 0.5).delay(0.1), value: currentIndex)
|
||||
|
||||
Text(item.subTitle)
|
||||
.font(.system(size: 14))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal,15)
|
||||
.foregroundColor(.gray)
|
||||
.offset(x: offset)
|
||||
.animation(.easeInOut(duration: 0.5).delay(0.2), value: currentIndex)
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
// MARK: Next / Login Button
|
||||
VStack(spacing: 15){
|
||||
Button {
|
||||
if currentIndex < onboardingItems.count - 1{
|
||||
// MARK: Pausing Previous Animation
|
||||
let currentProgress = onboardingItems[currentIndex].lottieView.currentProgress
|
||||
onboardingItems[currentIndex].lottieView.currentProgress = (currentProgress == 0 ? 0.7 : currentProgress)
|
||||
currentIndex += 1
|
||||
// MARK: Playing Next Animation from Start
|
||||
playAnimation()
|
||||
}
|
||||
if isLastSlide {
|
||||
withAnimation(){
|
||||
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
|
||||
|
||||
Text(isLastSlide ? "登录" : "下一步")
|
||||
.foregroundColor(.white)
|
||||
.padding(.vertical, isLastSlide ? 13 : 12)
|
||||
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(Color("Green"))
|
||||
)
|
||||
.frame(maxWidth: .infinity) // 保证按钮宽度填充父视图
|
||||
.padding(.horizontal, isLastSlide ? 50 : 100)
|
||||
.contentShape(Rectangle()) // 扩大点击区域
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Button(isLastSlide ? "登录" : "下一步"){
|
||||
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.padding(.vertical,isLastSlide ? 13 : 12)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background {
|
||||
Capsule()
|
||||
.fill(Color("Green"))
|
||||
}
|
||||
.padding(.horizontal,isLastSlide ? 50 : 100)
|
||||
*/
|
||||
|
||||
if currentIndex == onboardingItems.count - 1{
|
||||
|
||||
HStack{
|
||||
// Button("Terms of Service"){}
|
||||
|
||||
// Button("Privacy Policy"){}
|
||||
}
|
||||
.font(.caption2)
|
||||
//.underline(true, color: .primary)
|
||||
.offset(y: 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
.animation(.easeInOut, value: isLastSlide)
|
||||
.padding(15)
|
||||
.frame(width: size.width, height: size.height)
|
||||
}
|
||||
}
|
||||
.frame(width: size.width * CGFloat(onboardingItems.count),alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
func playAnimation(){
|
||||
onboardingItems[currentIndex].lottieView.currentProgress = 0
|
||||
onboardingItems[currentIndex].lottieView.play(toProgress: 0.7)
|
||||
}
|
||||
|
||||
// MARK: Retreving Index of the Item in the Array
|
||||
func indexOf(_ item: OnboardingItem)->Int{
|
||||
if let index = onboardingItems.firstIndex(of: item){
|
||||
return index
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
//struct OnBoardingScreen_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ContentView()
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// ResizableLottieView.swift
|
||||
// AnimatedOnBoardingScreen
|
||||
//
|
||||
// Created by zues on 17/12/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: Resizable Lottie View Without Background
|
||||
struct ResizableLottieView: UIViewRepresentable{
|
||||
@Binding var onboardingItem: OnboardingItem
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
setupLottieView(view)
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIView, context: Context) {
|
||||
|
||||
}
|
||||
|
||||
func setupLottieView(_ to: UIView){
|
||||
let lottieView = onboardingItem.lottieView
|
||||
lottieView.loopMode = .loop
|
||||
lottieView.backgroundColor = .clear
|
||||
lottieView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// MARK: Applying Constraints
|
||||
let constraints = [
|
||||
lottieView.widthAnchor.constraint(equalTo: to.widthAnchor),
|
||||
lottieView.heightAnchor.constraint(equalTo: to.heightAnchor),
|
||||
]
|
||||
to.addSubview(lottieView)
|
||||
to.addConstraints(constraints)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
//
|
||||
// OrderListView.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct OrderListView: View {
|
||||
|
||||
// @Binding var isPresented: Bool
|
||||
|
||||
@State private var planList: [OrderInfoDatum] = []
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
@State private var alert: Alert?
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode // 环境变量用于控制视图的呈现
|
||||
|
||||
var body: some View {
|
||||
NavigationView(content: {
|
||||
VStack {
|
||||
|
||||
// Navigation Bar
|
||||
ZStack{
|
||||
|
||||
|
||||
HStack{
|
||||
|
||||
Button(action: {
|
||||
|
||||
presentationMode.wrappedValue.dismiss() // 手动触发返回
|
||||
}, label: {
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("返回")
|
||||
.foregroundColor(.white)
|
||||
}).padding(10) // 增加可点击区域
|
||||
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
VStack(spacing: 5){
|
||||
|
||||
Text("我的订单")
|
||||
.fontWeight(.bold).lineLimit(1).frame(width: UIScreen.main.bounds.width*0.5)
|
||||
|
||||
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
|
||||
|
||||
}
|
||||
.padding(.all)
|
||||
|
||||
|
||||
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
.padding()
|
||||
}
|
||||
|
||||
if let errorMessage = errorMessage {
|
||||
Text(errorMessage)
|
||||
.bold()
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center) // 确保多行文本居中
|
||||
.padding()
|
||||
}
|
||||
|
||||
//
|
||||
// // Subscription Plans
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 16) {
|
||||
if planList.isEmpty {
|
||||
// Text("Loading plans...") // Loading indicator
|
||||
if !isLoading
|
||||
{
|
||||
Text("暂无记录").font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.multilineTextAlignment(.center) // 确保多行文本居中
|
||||
.padding()
|
||||
}
|
||||
} else {
|
||||
|
||||
ForEach(planList) { plan in
|
||||
|
||||
NavigationLink(destination: OrderPaymentView(orderinfo: plan )) {
|
||||
OrderItemView(orderinfo: plan)
|
||||
}
|
||||
//
|
||||
// Button(action: {
|
||||
//
|
||||
// }, label: {
|
||||
// OrderItemView(orderinfo: plan)
|
||||
// })
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}.padding(.top, 20)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Subscription Notice
|
||||
Text("订阅须知:仅支持微信和支付宝付款")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.background(
|
||||
BackgroundBg()
|
||||
)
|
||||
.onAppear(){
|
||||
Task{
|
||||
await getOrderList()
|
||||
}
|
||||
}.edgesIgnoringSafeArea(.bottom)
|
||||
|
||||
})
|
||||
.navigationBarHidden(true)
|
||||
.navigationBarBackButtonHidden(true) // 隐藏返回按钮
|
||||
.alertBinding($alert)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public func getOrderList() async {
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/order/fetch")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
|
||||
if let error = error {
|
||||
self.errorMessage = "数据请求失败: \(error.localizedDescription)"
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
self.errorMessage = "数据请求失败"
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Parse the user info response
|
||||
if let Subscribe = try? JSONDecoder().decode(OrderInfoReponse.self, from: data) {
|
||||
|
||||
if let data = Subscribe.data {
|
||||
planList = data
|
||||
}else{
|
||||
self.errorMessage = Subscribe.message ?? ""
|
||||
}
|
||||
|
||||
}else{
|
||||
self.errorMessage = "数据请求失败: JSON 解析错误"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct OrderItemView: View {
|
||||
var orderinfo : OrderInfoDatum
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(orderinfo.plan.name + " " + "\(orderinfo.plan.transferEnable ?? 0)GB/月")
|
||||
.font(.headline)
|
||||
.foregroundColor(.black).multilineTextAlignment(.leading)
|
||||
|
||||
|
||||
Text("订单号:"+orderinfo.tradeNo)
|
||||
.font(.headline)
|
||||
.foregroundColor(.black).multilineTextAlignment(.leading)
|
||||
//.fontWeight(orderinfo.status == 2 ? .bold : .regular)
|
||||
|
||||
|
||||
Text("订单金额:¥\(String(format: "%.2f", orderinfo.totalAmount/100))")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.black)
|
||||
|
||||
HStack (spacing: 4) {
|
||||
Text(orderinfo.status_zh)
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
.padding(2)
|
||||
.background(orderinfo.status == 1 ? Color.green: (orderinfo.status == 0 ? Color.red : Color.gray) )
|
||||
.cornerRadius(4)
|
||||
.foregroundColor(.white)
|
||||
.offset(y:10)
|
||||
|
||||
Text(orderinfo.period_zh)
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
.padding(2)
|
||||
.background(Color("Main"))
|
||||
.cornerRadius(4)
|
||||
.foregroundColor(.white)
|
||||
// .offset(x: -16, y:16)
|
||||
.offset(y:10)
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
Text("创建时间:"+TimestampConverter.convertTimestampToDateString(orderinfo.createdAt))
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
.padding(2)
|
||||
.foregroundColor(.gray)
|
||||
// .offset(x: -16, y:16)
|
||||
.offset(y:10)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(10)
|
||||
.shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 3)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
|
||||
//struct SubscriptionView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// SubscriptionView()
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,445 @@
|
||||
//
|
||||
// OrderPaymentView.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct OrderPaymentView: View {
|
||||
var orderinfo : OrderInfoDatum
|
||||
|
||||
@State private var alert: Alert?
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
@State private var showAlert = false
|
||||
@State private var payment :PaymentReponseDatum?
|
||||
|
||||
|
||||
@State private var ischeckout = false
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode // 环境变量用于控制视图的呈现
|
||||
|
||||
var body: some View {
|
||||
NavigationView(content: {
|
||||
VStack{
|
||||
|
||||
// Navigation Bar
|
||||
ZStack{
|
||||
|
||||
HStack{
|
||||
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss() // 手动触发返回
|
||||
}, label: {
|
||||
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("返回")
|
||||
.foregroundColor(.white)
|
||||
}).padding(10) // 增加可点击区域
|
||||
|
||||
Spacer()
|
||||
if orderinfo.status == 0 {
|
||||
|
||||
Button(action: {
|
||||
alert = Alert(YesOrNOMessage: "确定取消订单吗?"){
|
||||
Task{
|
||||
await cancelorder(orderNO: orderinfo.tradeNo)
|
||||
}
|
||||
}
|
||||
}, label: {
|
||||
|
||||
Text("取消订单")
|
||||
.foregroundColor(.red)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
|
||||
Text("订单详情")
|
||||
.fontWeight(.bold).lineLimit(1).frame(width: UIScreen.main.bounds.width*0.5)
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.all)
|
||||
|
||||
|
||||
|
||||
VStack(spacing: 20) {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
// Product Details Section
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("订单详情")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
|
||||
HStack {
|
||||
Text("订单号: ")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white)
|
||||
Text(orderinfo.tradeNo)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("创建时间: ")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white)
|
||||
Text(TimestampConverter.convertTimestampToDateString(orderinfo.createdAt))
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
Spacer().frame(height: 20)
|
||||
|
||||
Text("商品详情")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
|
||||
HStack {
|
||||
Text("商品名称: ")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white)
|
||||
Text(orderinfo.plan.name)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.yellow)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("类型/周期: ")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white)
|
||||
Text(orderinfo.period_zh)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("产品流量: ")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white)
|
||||
Text("\(orderinfo.plan.transferEnable ?? 0)GB/月")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
// Pending payment stamp (badge)
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(orderinfo.status_zh)
|
||||
.font(.subheadline)
|
||||
.padding(10)
|
||||
.background(orderinfo.status == 1 ? Color.green: (orderinfo.status == 0 ? Color.red : Color.gray))
|
||||
.cornerRadius(10)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.black.opacity(0.2))
|
||||
.cornerRadius(10)
|
||||
|
||||
// Payment Method Section
|
||||
VStack(alignment: .leading) {
|
||||
Text("支付方式")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
|
||||
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
}
|
||||
|
||||
if let errorMessage = errorMessage {
|
||||
Text(errorMessage)
|
||||
.bold()
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center) // 确保多行文本居中
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
}else{
|
||||
|
||||
Text("\(payment?.name ?? "") (\(payment?.handlingFeePercent ?? "")%手续费)")
|
||||
.font(.subheadline)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.red.opacity(0.3))
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.padding()
|
||||
.background(Color.black.opacity(0.2))
|
||||
.cornerRadius(10)
|
||||
|
||||
// Order Summary Section
|
||||
VStack(alignment: .leading) {
|
||||
Text("订单摘要")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
|
||||
HStack {
|
||||
Text("商品价格:")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
Text("¥\(String(format: "%.2f", orderinfo.totalAmount/100))")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("手续费:")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
Text("¥\(String(format: "%.2f", shouxufei()))")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("总计:")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
Text("¥\(String(format: "%.2f", total()))")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.black.opacity(0.2))
|
||||
.cornerRadius(10)
|
||||
|
||||
if orderinfo.status == 0 {
|
||||
// Pay Now Button
|
||||
Button(action: {
|
||||
// Trigger the payment action
|
||||
showAlert = true
|
||||
}, label: {
|
||||
HStack {
|
||||
Image(systemName: "cart")
|
||||
Text("立即支付")
|
||||
if ischeckout {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle()).tint(.white)
|
||||
}
|
||||
|
||||
}
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.cornerRadius(10)
|
||||
|
||||
}).disabled(ischeckout)
|
||||
.padding()
|
||||
.alert(isPresented: $showAlert) {
|
||||
Alert(
|
||||
title: Text("提示"),
|
||||
message: Text("确认要支付吗?"),
|
||||
primaryButton: .default(Text("确定"),action: {
|
||||
Task{
|
||||
if let paymentID = payment?.id {
|
||||
await submitOrder(orderNO:orderinfo.tradeNo,paymentID:paymentID)
|
||||
}
|
||||
|
||||
}
|
||||
}),
|
||||
secondaryButton: .cancel(Text("取消"))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.navigationBarHidden(true)
|
||||
.onAppear(){
|
||||
Task{
|
||||
await getPayment()
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
.background(
|
||||
BackgroundBg()
|
||||
)
|
||||
})
|
||||
.navigationBarHidden(true)
|
||||
.navigationBarBackButtonHidden(true) // 隐藏返回按钮
|
||||
.alertBinding($alert)
|
||||
}
|
||||
|
||||
func shouxufei() -> Double {
|
||||
return orderinfo.totalAmount/100 * (Double(payment?.handlingFeePercent ?? "") ?? 0.0)/100
|
||||
}
|
||||
|
||||
func total() -> Double{
|
||||
return orderinfo.totalAmount/100 + shouxufei()
|
||||
}
|
||||
|
||||
func cancelorder(orderNO:String) async {
|
||||
//cancel?trade_no=2024102108102956235125484
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/order/cancel?trade_no=\(orderNO)")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
|
||||
if let error = error {
|
||||
alert = Alert(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
alert = Alert(errorMessage: "取消失败,返回数据为空")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
struct cancelResponse: Codable {
|
||||
let status, message: String?
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Parse the user info response
|
||||
if let message = try? JSONDecoder().decode(cancelResponse.self, from: data) {
|
||||
if let status = message.status,status == "success" {
|
||||
alert = Alert(okMessage: "取消成功", {
|
||||
presentationMode.wrappedValue.dismiss() // 手动触发返回
|
||||
})
|
||||
}else{
|
||||
alert = Alert(errorMessage: message.message ?? "" )
|
||||
}
|
||||
|
||||
//2024101311320180720&sitename=
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
func submitOrder(orderNO:String,paymentID:Int) async {
|
||||
ischeckout = true
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/order/checkout?trade_no=\(orderNO)&method=\(paymentID)")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
ischeckout = false
|
||||
if let error = error {
|
||||
alert = Alert(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
alert = Alert(errorMessage: "支付失败,返回数据为空")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
struct saveOrderResponse: Codable {
|
||||
let status, message, data: String?
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Parse the user info response
|
||||
if let message = try? JSONDecoder().decode(saveOrderResponse.self, from: data) {
|
||||
if let payurl = message.data,payurl.count > 10 {
|
||||
openURL(URL(string:payurl)!)
|
||||
}else{
|
||||
alert = Alert(errorMessage: message.message ?? "" )
|
||||
}
|
||||
|
||||
//2024101311320180720&sitename=
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
func getPayment() async {
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/order/getPaymentMethod")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
|
||||
if let error = error {
|
||||
self.errorMessage = "数据请求失败: \(error.localizedDescription)"
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
self.errorMessage = "数据请求失败"
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Parse the user info response
|
||||
if let Subscribe = try? JSONDecoder().decode(PaymentReponse.self, from: data) {
|
||||
|
||||
if let data = Subscribe.data {
|
||||
payment = data.first
|
||||
|
||||
}else{
|
||||
self.errorMessage = Subscribe.message ?? ""
|
||||
}
|
||||
|
||||
}else{
|
||||
self.errorMessage = "数据请求失败: JSON 解析错误"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
}
|
||||
//struct PaymentView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// OrderPaymentView()
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,183 @@
|
||||
//
|
||||
// SettingsAboutView.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/12.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsAboutView: View {
|
||||
@State private var alert: Alert?
|
||||
@Environment(\.openURL) private var openURL // 引入环境值
|
||||
@Environment(\.presentationMode) var presentationMode // 环境变量用于控制视图的呈现
|
||||
var body: some View {
|
||||
|
||||
NavigationView(content: {
|
||||
VStack() {
|
||||
|
||||
ZStack{
|
||||
|
||||
HStack{
|
||||
Button(action: {
|
||||
|
||||
presentationMode.wrappedValue.dismiss() // 手动触发返回
|
||||
}, label: {
|
||||
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("返回")
|
||||
.foregroundColor(.white)
|
||||
}).padding(10) // 增加可点击区域
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
|
||||
Text("关于我们")
|
||||
.fontWeight(.bold).lineLimit(1)
|
||||
}
|
||||
.padding()
|
||||
// .padding(.top,edges.top)
|
||||
.foregroundColor(.white)
|
||||
|
||||
|
||||
Spacer().frame(height: 40)
|
||||
// 顶部的Logo和标题
|
||||
VStack {
|
||||
Image("applogo")
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.clipShape(Circle())
|
||||
.foregroundColor(.orange) // 使用系统图标,实际图标替换时可以用 Image("logo")
|
||||
|
||||
|
||||
Text("UUVPN for iOS \n在您的 iPhone 和 iPad 上体验最快的全球网络连接工具")
|
||||
.font(.subheadline)
|
||||
.padding(.top, 10)
|
||||
.frame(width: 300)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
Spacer().frame(height: 40)
|
||||
|
||||
// 设置项列表
|
||||
List {
|
||||
|
||||
HStack {
|
||||
Text("当前版本")
|
||||
Spacer()
|
||||
Text( Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0").foregroundColor(.gray)
|
||||
}.padding(.vertical, 10).listRowBackground(Color.clear)
|
||||
|
||||
Button(action: {
|
||||
alert = Alert(okMessage: "没有检测到更新,已经是最新版本.")
|
||||
}, label: {
|
||||
HStack {
|
||||
Text("检查更新")
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right").foregroundColor(.gray)
|
||||
}.padding(.vertical, 10)
|
||||
}).listRowBackground(Color.clear)
|
||||
|
||||
|
||||
|
||||
Button(action: {
|
||||
if let url = URL(string: UserManager.shared.websiteURL()) {
|
||||
openURL(url) { accepted in // 通过设置 completion 闭包,可以检查是否已完成 URL 的开启。状态由 OpenURLAction 提供
|
||||
print(accepted ? "Success" : "Failure")
|
||||
}
|
||||
}
|
||||
}, label: {
|
||||
HStack {
|
||||
Text("官方网址")
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right").foregroundColor(.gray)
|
||||
}.padding(.vertical, 10)
|
||||
}).listRowBackground(Color.clear)
|
||||
|
||||
|
||||
Button(action: {
|
||||
if let url = URL(string: UserManager.shared.telegramUrl()) {
|
||||
openURL(url) { accepted in // 通过设置 completion 闭包,可以检查是否已完成 URL 的开启。状态由 OpenURLAction 提供
|
||||
print(accepted ? "Success" : "Failure")
|
||||
}
|
||||
}
|
||||
}, label: {
|
||||
|
||||
HStack {
|
||||
Text("订阅Telegram频道")
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right").foregroundColor(.gray)
|
||||
}.padding(.vertical, 10)
|
||||
}).listRowBackground(Color.clear)
|
||||
|
||||
|
||||
}
|
||||
.listStyle(PlainListStyle()) // 设置为普通列表样式
|
||||
|
||||
Spacer()
|
||||
|
||||
// 底部的版权信息和链接
|
||||
VStack {
|
||||
HStack {
|
||||
Button(action: {
|
||||
// 打开用户协议链接
|
||||
if let url = URL(string: "https://minipanda.soccertt.com/teams.html") {
|
||||
openURL(url) { accepted in // 通过设置 completion 闭包,可以检查是否已完成 URL 的开启。状态由 OpenURLAction 提供
|
||||
print(accepted ? "Success" : "Failure")
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text("用户协议")
|
||||
.foregroundColor(.white)
|
||||
.font(.subheadline)
|
||||
.underline()
|
||||
}
|
||||
Button(action: {
|
||||
// 打开隐私政策链接
|
||||
if let url = URL(string: "https://minipanda.soccertt.com/privacy.html") {
|
||||
openURL(url) { accepted in // 通过设置 completion 闭包,可以检查是否已完成 URL 的开启。状态由 OpenURLAction 提供
|
||||
print(accepted ? "Success" : "Failure")
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text("隐私政策")
|
||||
.foregroundColor(.white)
|
||||
.font(.subheadline)
|
||||
.underline()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
Text("Copyright 2014 - 2024, UUVPN 版权所有")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
.padding(.top, 5)
|
||||
}
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
.background(
|
||||
BackgroundBg()
|
||||
)
|
||||
.alertBinding($alert)
|
||||
// .padding()
|
||||
.navigationBarHidden(true)
|
||||
// .edgesIgnoringSafeArea(.bottom)
|
||||
})
|
||||
.navigationBarHidden(true)
|
||||
.navigationBarBackButtonHidden(true) // 隐藏返回按钮
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsAboutView()
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
//
|
||||
// SideMenuView.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/12.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import ApplicationLibrary
|
||||
import WebKit
|
||||
|
||||
import Libbox
|
||||
import Library
|
||||
|
||||
struct SideMenuView: View {
|
||||
|
||||
@Binding var isPresented: Bool
|
||||
@Binding var isLoggedIn: Bool
|
||||
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
|
||||
@State private var plandes: SubscribeReponseClass?
|
||||
@State private var size: CGSize = .zero
|
||||
@State private var avator = UserManager.shared.getUserInfo().avator
|
||||
@State private var emailid = UserManager.shared.getUserInfo().email
|
||||
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
@State private var alert: Alert?
|
||||
|
||||
|
||||
@State private var showAlert = false
|
||||
@State private var isInviteActive = false
|
||||
|
||||
@State private var isLogouting = false
|
||||
|
||||
|
||||
@AppStorage("paymentURLKey") private var paymentURLKey = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack() {
|
||||
|
||||
// Navigation Bar
|
||||
// 用户信息区域
|
||||
ZStack{
|
||||
|
||||
HStack{
|
||||
Button(action: {
|
||||
isPresented = false
|
||||
}, label: {
|
||||
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("返回")
|
||||
.foregroundColor(.white)
|
||||
}).padding(10)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
|
||||
Text("设置")
|
||||
.fontWeight(.bold).lineLimit(1)
|
||||
}
|
||||
.padding()
|
||||
// .padding(.top,edges.top)
|
||||
.foregroundColor(.white)
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
|
||||
// Profile Section
|
||||
VStack {
|
||||
|
||||
|
||||
HStack {
|
||||
|
||||
// AsyncImage(url: URL(string: avator)){ phase in
|
||||
// if let image = phase.image {
|
||||
// image // Displays the loaded image.
|
||||
// } else if phase.error != nil {
|
||||
// Color.red // Indicates an error.
|
||||
// } else {
|
||||
// Color.blue // Acts as a placeholder.
|
||||
// }
|
||||
// }.frame(width: 50, height: 50)
|
||||
// .clipShape(Circle())
|
||||
// Image("applogo").resizable().frame(width: 50, height: 50).clipShape(Circle())
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("账户信息:")
|
||||
.font(.headline).fontWeight(.light)
|
||||
|
||||
Text(emailid)
|
||||
.font(.headline)
|
||||
.bold()
|
||||
|
||||
}
|
||||
Spacer()
|
||||
/*
|
||||
Spacer()
|
||||
Button {
|
||||
showAlert = true
|
||||
} label: {
|
||||
|
||||
Text("退出登录").foregroundColor(.red)
|
||||
Image(systemName: "rectangle.portrait.and.arrow.forward")
|
||||
.foregroundColor(.red)
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 4, height: 4)
|
||||
|
||||
}*/
|
||||
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white.opacity(0.1))
|
||||
.cornerRadius(12)
|
||||
.shadow(color: .gray.opacity(0.1), radius: 5, x: 0, y: 5)
|
||||
|
||||
if (paymentURLKey.count > 3){
|
||||
HStack {
|
||||
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
.padding()
|
||||
}
|
||||
|
||||
if let errorMessage = errorMessage {
|
||||
Text(errorMessage)
|
||||
.bold()
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding()
|
||||
}else{
|
||||
|
||||
if let plandesss = self.plandes {
|
||||
if plandesss.transferEnable > 0 {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
|
||||
// Text("订阅详情:") .font(.subheadline)
|
||||
|
||||
|
||||
Text(plandesss.plan?.name ?? "")
|
||||
.font(.subheadline)
|
||||
let u = plandesss.d/1024/1024/1024
|
||||
let t = plandesss.transferEnable/1024/1024/1024
|
||||
|
||||
Spacer().frame(height: 10)
|
||||
|
||||
Text("已用: \(u) GB / 总计:\(t) GB")
|
||||
.font(.subheadline).bold()
|
||||
|
||||
HStack {
|
||||
// White bar on the left
|
||||
// RoundedRectangle(cornerRadius: 10)
|
||||
// .fill(Color.red)
|
||||
// .frame(width: 300*CGFloat(plandesss.d)/CGFloat(plandesss.transferEnable), height: 20)
|
||||
//
|
||||
Text("").frame(height: 20).frame(minWidth: (CGFloat(plandesss.d)/CGFloat(plandesss.transferEnable))<=0.1 ? 20 : 300*(CGFloat(plandesss.d)/CGFloat(plandesss.transferEnable))).background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(
|
||||
Color.red
|
||||
)
|
||||
)
|
||||
Spacer()
|
||||
}
|
||||
.frame(height: 20)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(
|
||||
Color.gray.opacity(0.5)
|
||||
)
|
||||
)
|
||||
.padding()
|
||||
|
||||
|
||||
|
||||
|
||||
// ScrollView {
|
||||
// }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||
// AttributedText(htmlContent: plan.content, size: $size)
|
||||
// .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, idealHeight: size.height, maxHeight: .infinity)
|
||||
// .background(.clear)
|
||||
|
||||
}
|
||||
Spacer()
|
||||
}else{
|
||||
Spacer()
|
||||
Text("暂无订阅")
|
||||
.font(.subheadline)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}.padding()
|
||||
.background(Color.white.opacity(0.1))
|
||||
.cornerRadius(12)
|
||||
.shadow(color: .gray.opacity(0.1), radius: 5, x: 0, y: 5)
|
||||
}
|
||||
} .frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.foregroundColor(.white)
|
||||
|
||||
// Menu Items
|
||||
// List {
|
||||
// NavigationLink(destination: WebShuntView()) {
|
||||
// MenuItem(icon: "globe", title: "App日志", color: .white, isNew: false)
|
||||
// }
|
||||
LazyVStack(spacing: 10, content: {
|
||||
NavigationLink(destination: SupportTicketView()) {
|
||||
MenuItem(icon: "listclipboard", title: "我的工单", color: .white, isNew: false)
|
||||
}
|
||||
Divider()
|
||||
|
||||
if (paymentURLKey.count > 3){
|
||||
|
||||
NavigationLink(destination: UpgradeView()) {
|
||||
MenuItem(icon: "crown.fill", title: "升级套餐", color: .yellow, isNew: true)
|
||||
}
|
||||
Divider()
|
||||
NavigationLink(destination: OrderListView()) {
|
||||
MenuItem(icon: "checkout", title: "我的订单", color: .white, isNew: false)
|
||||
}
|
||||
|
||||
Divider()
|
||||
NavigationLink(destination: InviteListView(isPresented: $isInviteActive)) {
|
||||
MenuItem(icon: "star.fill", title: "邀请中心", color: .white, isNew: false)
|
||||
}
|
||||
Divider()
|
||||
|
||||
|
||||
}else{
|
||||
|
||||
NavigationLink(destination: MessageCenterView()) {
|
||||
MenuItem(icon: "message.badge.circle", title: "消息通知", color: .white, isNew: false)
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
|
||||
|
||||
NavigationLink(destination: QuestionView()) {
|
||||
MenuItem(icon: "questionmark.app", title: "问题解答", color: .white, isNew: false)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
NavigationLink(destination: SupportView()) {
|
||||
MenuItem(icon: "headphones", title: "联系客服", color: .white, isNew: false)
|
||||
}
|
||||
Divider()
|
||||
NavigationLink(destination: AboutUsView()) {
|
||||
MenuItem(icon: "info.circle.fill", title: "关于我们", color: .white, isNew: false)
|
||||
}
|
||||
Divider()
|
||||
}).padding()
|
||||
|
||||
Button(action: {
|
||||
// Trigger the payment action
|
||||
// showAlert = true
|
||||
|
||||
alert = Alert(YesOrNOMessage: "确定退出登录吗?", {
|
||||
isLogouting=true
|
||||
UserManager.shared.clearUserData()
|
||||
// 删除订阅信息
|
||||
Task {
|
||||
//await reloadSubscribe()
|
||||
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
|
||||
isLoggedIn = false
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}, label: {
|
||||
HStack {
|
||||
Image(systemName: "trash.fill")
|
||||
Text("退出登录")
|
||||
|
||||
}
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.cornerRadius(10)
|
||||
|
||||
}).padding()
|
||||
|
||||
}
|
||||
|
||||
// }
|
||||
// .listStyle(PlainListStyle())
|
||||
|
||||
}.background(
|
||||
BackgroundBg()
|
||||
).modifier(ActivityIndicatorModifier(isLoading: isLogouting, color: Color.black.opacity(0.8), lineWidth: 1))
|
||||
.onAppear {
|
||||
Task {
|
||||
await reloadSubscribe()
|
||||
}
|
||||
}.navigationBarHidden(true)
|
||||
|
||||
}.alertBinding($alert)
|
||||
|
||||
}
|
||||
|
||||
private func writeProfilePreviewList() async throws {
|
||||
let profiles = try await ProfileManager.list()
|
||||
|
||||
for profile in profiles {
|
||||
print("detele : \(profile.path) \(profile.remoteURL ?? "" )")
|
||||
await deleteProfile(profile)
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteProfile(_ profile: Profile) async {
|
||||
do {
|
||||
_ = try await ProfileManager.delete(profile)
|
||||
} catch {
|
||||
alert = Alert(error)
|
||||
return
|
||||
}
|
||||
environments.profileUpdate.send()
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
public func reloadSubscribe() async {
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/getSubscribe")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
|
||||
if let error = error {
|
||||
self.errorMessage = "套餐数据请求失败: \(error.localizedDescription)"
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
self.errorMessage = "套餐数据请求失败:数据为空"
|
||||
return
|
||||
}
|
||||
// Parse the user info response
|
||||
if let Subscribe = try? JSONDecoder().decode(SubscribeReponse.self, from: data) {
|
||||
|
||||
if let data = Subscribe.data {
|
||||
|
||||
plandes = data
|
||||
emailid = data.email
|
||||
|
||||
UserManager.shared.storeUserInfo(email: emailid, avator: "")
|
||||
// UserManager.shared.storeSuburlData(data: data.subscribeURL)
|
||||
|
||||
}else{
|
||||
self.errorMessage = Subscribe.message ?? ""
|
||||
}
|
||||
|
||||
|
||||
}else{
|
||||
self.errorMessage = "套餐数据请求失败"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Sample Destination Views for each menu item
|
||||
struct UpgradeView: View {
|
||||
@State private var isSubscriptionActive = false
|
||||
var body: some View {
|
||||
SubscriptionView(isPresented: $isSubscriptionActive)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct WebShuntView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .trailing, content: {
|
||||
LogView()
|
||||
ProfileView()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
struct Free30DaysView: View {
|
||||
@State private var islogined = false
|
||||
var body: some View {
|
||||
InviteView(isPresented: $islogined)
|
||||
}
|
||||
}
|
||||
|
||||
struct AboutUsView: View {
|
||||
var body: some View {
|
||||
SettingsAboutView()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct MenuItem: View {
|
||||
let icon: String
|
||||
let title: String
|
||||
let color: Color
|
||||
let isNew: Bool?
|
||||
|
||||
var body: some View {
|
||||
// Button {
|
||||
//
|
||||
// } label: {
|
||||
//
|
||||
// }
|
||||
|
||||
HStack {
|
||||
|
||||
if UIImage(systemName: icon) != nil{
|
||||
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(color).tint(.white)
|
||||
.frame(width: 24, height: 24)
|
||||
|
||||
}else{
|
||||
|
||||
Image(icon).resizable()
|
||||
.foregroundColor(.white).tint(.white)
|
||||
.frame(width: 24, height: 24)
|
||||
|
||||
}
|
||||
Text(title)
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 18))
|
||||
Spacer()
|
||||
|
||||
if isNew == true {
|
||||
Circle()
|
||||
.fill(Color.red)
|
||||
.frame(width: 8, height: 8)
|
||||
}
|
||||
}
|
||||
|
||||
.padding(.vertical, 10)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct AttributedText: UIViewRepresentable {
|
||||
let htmlContent: String
|
||||
@Binding var size: CGSize
|
||||
|
||||
private let webView = WKWebView()
|
||||
var sizeObserver: NSKeyValueObservation?
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
webView.scrollView.isScrollEnabled = false //<-- Here
|
||||
webView.navigationDelegate = context.coordinator
|
||||
return webView
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: WKWebView, context: Context) {
|
||||
uiView.loadHTMLString(htmlContent, baseURL: nil)
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(parent: self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, WKNavigationDelegate {
|
||||
let parent: AttributedText
|
||||
var sizeObserver: NSKeyValueObservation?
|
||||
|
||||
init(parent: AttributedText) {
|
||||
self.parent = parent
|
||||
sizeObserver = parent.webView.scrollView.observe(\.contentSize, options: [.new], changeHandler: { (object, change) in
|
||||
parent.size = change.newValue ?? .zero
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
//
|
||||
// SubscriptionView.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/12.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct SubscriptionView: View {
|
||||
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
@State private var planList: [DatuPlanResponse] = []
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
@State private var alert: Alert?
|
||||
@State var orderinfo : OrderInfoDatum?
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
let adimages = ["10-35-24", "10-35-15", "10-35-03"] // 替换为你广告图片的名字
|
||||
let adimagesString = ["UUVPN 采用最高安全 ECC 加密技术,仅需一键操作,军事级的加密技术将护航您的任何互联网访问", "UUVPN 拥有170 个热门城市的300+高速服务器,您可以随时随地获得高速安全的互联网体验", "UUVPN 支持所有当前主流的平台系统,您可以在几乎任何设备上使用"]
|
||||
@Environment(\.presentationMode) var presentationMode // 环境变量用于控制视图的呈现
|
||||
// 当前页的索引
|
||||
|
||||
|
||||
@State private var isNavigationActive = false // 控制跳转
|
||||
|
||||
@State private var currentIndex = 0
|
||||
// 自动轮播计时器
|
||||
let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
|
||||
|
||||
var body: some View {
|
||||
NavigationView(content: {
|
||||
VStack {
|
||||
ZStack{
|
||||
|
||||
|
||||
HStack{
|
||||
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
isPresented = false // Close the side menu
|
||||
}
|
||||
presentationMode.wrappedValue.dismiss() // 手动触发返回
|
||||
}, label: {
|
||||
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("返回")
|
||||
.foregroundColor(.white)
|
||||
}).padding(10) // 增加可点击区域
|
||||
|
||||
Spacer()
|
||||
|
||||
NavigationLink(destination: OrderListView()) {
|
||||
Text("我的订单").foregroundColor(.red)
|
||||
}.listRowBackground(Color.clear)
|
||||
|
||||
}
|
||||
Spacer()
|
||||
VStack(spacing: 5){
|
||||
|
||||
Text( "升级套餐")
|
||||
.fontWeight(.bold).lineLimit(1).frame(width: UIScreen.main.bounds.width*0.5)
|
||||
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
|
||||
}
|
||||
.padding(.all)
|
||||
|
||||
if let orderinfoS = orderinfo{
|
||||
|
||||
NavigationLink(destination: OrderPaymentView(orderinfo: orderinfoS),isActive: $isNavigationActive, label: {
|
||||
EmptyView() // 隐藏的视图
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
//
|
||||
// // Subscription Plans
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
// VPN Info and Trial Offer
|
||||
VStack(spacing: 8) {
|
||||
// 图片轮播
|
||||
TabView(selection: $currentIndex) {
|
||||
ForEach(0..<adimages.count, id: \.self) { index in
|
||||
VStack(spacing: 8) {
|
||||
Image(adimages[index])
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: UIScreen.main.bounds.width * 0.8,height: 150)
|
||||
.cornerRadius(10)
|
||||
.tag(index)
|
||||
|
||||
|
||||
Text(adimagesString[index])
|
||||
.font(.subheadline)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.gray)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) // 隐藏系统自带的指示器
|
||||
.frame(height: 220)
|
||||
.onReceive(timer) { _ in
|
||||
// 自动切换广告
|
||||
withAnimation {
|
||||
currentIndex = (currentIndex + 1) % adimages.count
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义指示器
|
||||
HStack(spacing: 8) {
|
||||
ForEach(0..<adimages.count, id: \.self) { index in
|
||||
Circle()
|
||||
.fill(index == currentIndex ? Color.green : Color.gray)
|
||||
.frame(width: 5, height: 5)
|
||||
}
|
||||
}
|
||||
// .padding(.top, 3)
|
||||
|
||||
|
||||
// Free Trial Text
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "checkmark.shield.fill")
|
||||
.foregroundColor(.green)
|
||||
Text("首次订阅免费试用 2 天")
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
|
||||
}
|
||||
.padding(.top, 10)
|
||||
|
||||
Text("订阅可随时取消,试用期内取消不收取任何费用")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.padding(.top, 20)
|
||||
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
.padding()
|
||||
}
|
||||
|
||||
if let errorMessage = errorMessage {
|
||||
Text(errorMessage)
|
||||
.bold()
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center) // 确保多行文本居中
|
||||
.padding()
|
||||
}
|
||||
VStack(spacing: 16) {
|
||||
if planList.isEmpty {
|
||||
// Text("Loading plans...") // Loading indicator
|
||||
} else {
|
||||
|
||||
ForEach(planList) { plan in
|
||||
|
||||
Button(action: {
|
||||
//print( plan.content ?? "")
|
||||
Task{
|
||||
await subPlanOrder(plan_id:plan.idOLD ?? 0,amount : plan.monthPrice ?? 0)
|
||||
}
|
||||
}, label: {
|
||||
|
||||
SubscriptionPlanView(planName: plan.name, originalPrice: "¥\(String(format: "%.2f", Double(plan.monthPrice ?? 0)/100)) /月", discountedPrice: "", discountPercentage: "", monthlyPrice: "", content: plan.content ?? "", bestPlan: plan.idOLD == planList.count,planContent: "\(plan.transferEnable ?? 0)GB/月")
|
||||
|
||||
// Text( plan.content ?? "").padding()
|
||||
|
||||
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}.padding(.top, 20)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Subscription Notice
|
||||
Text("订阅须知:仅支持微信和支付宝付款")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
.background(
|
||||
LinearGradient(colors: [
|
||||
|
||||
Color("BG1"),
|
||||
Color("BG1"),
|
||||
Color("BG2"),
|
||||
Color("BG2"),
|
||||
|
||||
], startPoint: .top, endPoint: .bottom)
|
||||
)
|
||||
// .background(.white)
|
||||
.onAppear(){
|
||||
Task{
|
||||
await getPlanList()
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
|
||||
})
|
||||
.navigationBarHidden(true)
|
||||
.alertBinding($alert)
|
||||
.navigationBarBackButtonHidden(true) // 隐藏返回按钮
|
||||
}
|
||||
|
||||
|
||||
|
||||
public func subPlanOrder(plan_id: Int,amount: Int) async {
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/order/save?plan_id=\(plan_id)&period=month_price&coupon_code=")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
|
||||
if let error = error {
|
||||
alert = Alert(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
alert = Alert(errorMessage: "提交订单信息错误,请重试")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
struct saveOrderResponse: Codable {
|
||||
let status, message, data: String?
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Parse the user info response
|
||||
if let message = try? JSONDecoder().decode(saveOrderResponse.self, from: data) {
|
||||
if let dataNO = message.data,dataNO.count > 10 {
|
||||
Task{
|
||||
await getOrderInfo(OrderNO: dataNO)
|
||||
}
|
||||
|
||||
|
||||
}else{
|
||||
alert = Alert(errorMessage: message.message ?? "" )
|
||||
}
|
||||
|
||||
//2024101311320180720&sitename=
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
|
||||
|
||||
}
|
||||
|
||||
func getOrderInfo(OrderNO: String) async {
|
||||
|
||||
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/order/detail?trade_no=\(OrderNO)")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
|
||||
if let error = error {
|
||||
self.errorMessage = "数据请求失败: \(error.localizedDescription)"
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
self.errorMessage = "数据请求失败"
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the user info response
|
||||
if let Subscribe = try? JSONDecoder().decode(OrderInfoSingleReponse.self, from: data) {
|
||||
|
||||
if let data = Subscribe.data {
|
||||
orderinfo = data
|
||||
isNavigationActive = true
|
||||
}else{
|
||||
self.errorMessage = Subscribe.message ?? ""
|
||||
}
|
||||
|
||||
}else{
|
||||
self.errorMessage = "数据请求失败: JSON 解析错误"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
func getPlanList() async {
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/plan/fetch")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
|
||||
if let error = error {
|
||||
self.errorMessage = "数据请求失败: \(error.localizedDescription)"
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
self.errorMessage = "数据请求失败"
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the user info response
|
||||
if let Subscribe = try? JSONDecoder().decode(PlanResponse.self, from: data) {
|
||||
|
||||
if let data = Subscribe.data {
|
||||
planList = data
|
||||
}else{
|
||||
self.errorMessage = Subscribe.message ?? ""
|
||||
}
|
||||
|
||||
}else{
|
||||
self.errorMessage = "数据请求失败: JSON 解析错误"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct SubscriptionPlanView: View {
|
||||
var planName: String
|
||||
var originalPrice: String
|
||||
var discountedPrice: String
|
||||
var discountPercentage: String
|
||||
var monthlyPrice: String
|
||||
|
||||
var content: String
|
||||
var bestPlan: Bool = false
|
||||
var planContent: String
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack(spacing: 5, content: {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(planName)
|
||||
.font(.headline)
|
||||
.fontWeight(bestPlan ? .bold : .regular)
|
||||
.foregroundColor(bestPlan ? .green : .black)
|
||||
Text(planContent).font(.subheadline).fontWeight(bestPlan ? .bold : .regular).foregroundColor( .black)
|
||||
if bestPlan {
|
||||
Text("Best")
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
.padding(4)
|
||||
.background(Color.red)
|
||||
.cornerRadius(4)
|
||||
.foregroundColor(.white)
|
||||
.offset(x: -15, y:16)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
|
||||
|
||||
HStack {
|
||||
/* if bestPlan {
|
||||
Text("\(discountPercentage) OFF")
|
||||
.font(.caption)
|
||||
.padding(4)
|
||||
.foregroundColor(.white).background(.green)
|
||||
.cornerRadius(16)
|
||||
}else{
|
||||
Text("\(discountPercentage) OFF")
|
||||
.font(.caption)
|
||||
.padding(4)
|
||||
.foregroundColor(.black).background(.gray.opacity(0.5))
|
||||
.cornerRadius(16)
|
||||
}
|
||||
Text(discountedPrice)
|
||||
.strikethrough()
|
||||
.foregroundColor(.gray)
|
||||
|
||||
*/
|
||||
Text(originalPrice)
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.black)
|
||||
|
||||
}
|
||||
|
||||
Text(monthlyPrice)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
// Text(extractChinese(from: content)).font(.subheadline).foregroundStyle(.black).padding()
|
||||
//HTMLTextView(htmlString:content)
|
||||
|
||||
}).padding()
|
||||
|
||||
.background(Color.white.opacity(0.9))
|
||||
.cornerRadius(10)
|
||||
.shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 3)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
||||
func extractChinese(from text: String) -> String {
|
||||
let pattern = "[\\u4e00-\\u9fa5]+" // 匹配中文字符的正则表达式
|
||||
let regex = try? NSRegularExpression(pattern: pattern, options: [])
|
||||
let nsString = text as NSString
|
||||
let results = regex?.matches(in: text, options: [], range: NSRange(location: 0, length: nsString.length))
|
||||
|
||||
// 提取匹配的中文
|
||||
let chineseStrings = results?.compactMap { result -> String? in
|
||||
|
||||
if let range = Range(result.range, in: text) {
|
||||
return String(text[range])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
dump(chineseStrings)
|
||||
return chineseStrings?.joined(separator: " ") ?? ""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//struct SubscriptionView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// SubscriptionView()
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,749 @@
|
||||
//
|
||||
// SupportTicketChatView.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
// 1. 创建 KeyboardResponder 类
|
||||
class KeyboardResponder: ObservableObject {
|
||||
@Published var keyboardHeight: CGFloat = 0
|
||||
|
||||
private var cancellable: AnyCancellable?
|
||||
|
||||
|
||||
private var cancellableSet: Set<AnyCancellable> = []
|
||||
|
||||
init() {
|
||||
/*
|
||||
cancellable = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
|
||||
.merge(with: NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification))
|
||||
.sink { [weak self] notification in
|
||||
if let userInfo = notification.userInfo {
|
||||
|
||||
if notification.name == UIResponder.keyboardWillShowNotification,
|
||||
let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
|
||||
self?.keyboardHeight = keyboardFrame.cgRectValue.height
|
||||
print(" \(keyboardFrame.cgRectValue.height)" )
|
||||
}
|
||||
else {
|
||||
print(" \(notification.name)" )
|
||||
// self?.keyboardHeight = 0 // Reset height when keyboard hides
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/*
|
||||
let showPublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
|
||||
let hidePublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
|
||||
|
||||
showPublisher
|
||||
.merge(with: hidePublisher)
|
||||
.compactMap { notification in
|
||||
// 动画和键盘高度的处理
|
||||
guard let userInfo = notification.userInfo else { return 0 }
|
||||
let endFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
|
||||
return notification.name == UIResponder.keyboardWillShowNotification ? endFrame?.height ?? 0 : 0
|
||||
}
|
||||
.assign(to: \.keyboardHeight, on: self)
|
||||
.store(in: &cancellableSet)
|
||||
*/
|
||||
|
||||
NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
|
||||
.compactMap { $0.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect }
|
||||
.map { $0.height }
|
||||
.sink { [weak self] height in
|
||||
self?.keyboardHeight = height
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
|
||||
NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
|
||||
.map { _ in CGFloat.zero }
|
||||
.assign(to: &$keyboardHeight)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct SupportTicketChatView : View {
|
||||
|
||||
@State var message = ""
|
||||
@State var imagePicker = false
|
||||
@State var imgData : Data = Data(count: 0)
|
||||
@State private var alert: Alert?
|
||||
var ticketID:Int
|
||||
@State private var isMessageInputBarPresented = false
|
||||
|
||||
|
||||
//StateObject is the owner of the object....
|
||||
|
||||
@State private var alltickets: [Message] = []
|
||||
@State private var isLoading = false
|
||||
@State private var isSendingMsg = false
|
||||
@State private var errorMessage: String?
|
||||
@State private var showAlert = false
|
||||
@State private var tickchatData: TicketChatReponseDataClass?
|
||||
|
||||
@ObservedObject private var keyboard = KeyboardResponder()
|
||||
@StateObject var allMessages = Messages()
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode // 环境变量用于控制视图的呈现
|
||||
|
||||
var body: some View{
|
||||
NavigationView(content: {
|
||||
|
||||
|
||||
VStack{
|
||||
|
||||
Spacer()
|
||||
ZStack{
|
||||
|
||||
|
||||
HStack{
|
||||
|
||||
Button(action: {
|
||||
|
||||
presentationMode.wrappedValue.dismiss() // 手动触发返回
|
||||
}, label: {
|
||||
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("返回")
|
||||
.foregroundColor(.white)
|
||||
}).padding(10) // 增加可点击区域
|
||||
|
||||
Spacer()
|
||||
if tickchatData?.status == 0 {
|
||||
|
||||
|
||||
Button(action: {
|
||||
alert = Alert(YesOrNOMessage: "确定关闭工单吗?", {
|
||||
Task{
|
||||
await closeThisTicket()
|
||||
}
|
||||
|
||||
})
|
||||
}, label: {
|
||||
Text("关闭工单")
|
||||
.foregroundColor(.white)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
VStack(spacing: 5){
|
||||
|
||||
Text(tickchatData?.subject ?? "")
|
||||
.fontWeight(.bold).lineLimit(1).frame(width: UIScreen.main.bounds.width*0.5)
|
||||
|
||||
|
||||
if tickchatData?.status == 0 {
|
||||
Text("待回复").font(.caption).foregroundColor(.red)
|
||||
}else if tickchatData?.status == 1 {
|
||||
Text("已关闭").font(.caption).foregroundColor(.gray)
|
||||
}else if tickchatData?.status == 2 {
|
||||
Text("已回复").font(.caption).foregroundColor(.green)
|
||||
}
|
||||
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
|
||||
}
|
||||
.padding(.all)
|
||||
|
||||
|
||||
VStack{
|
||||
|
||||
// Displaying Message....
|
||||
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle()).tint(Color("Main"))
|
||||
.padding().padding(.horizontal)
|
||||
Spacer()
|
||||
|
||||
}
|
||||
|
||||
|
||||
if let errorMessage = errorMessage {
|
||||
Text(errorMessage)
|
||||
.bold()
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center) // 确保多行文本居中
|
||||
.padding(.horizontal)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (alltickets.count > 0 ){
|
||||
Spacer()
|
||||
ScrollView(.vertical, showsIndicators: false, content: {
|
||||
|
||||
ScrollViewReader{reader in
|
||||
|
||||
VStack(spacing: 20){
|
||||
|
||||
|
||||
ForEach(allMessages.messages){msg in
|
||||
|
||||
// Chat Bubbles...
|
||||
Text("\(TimestampConverter.convertTimestampToDateString(msg.updatedAt))").font(.caption).foregroundColor(.black.opacity(0.8))
|
||||
ChatBubble(msg: msg)
|
||||
|
||||
}
|
||||
// when ever a new data is inserted scroll to bottom...
|
||||
.onChange(of: allMessages.messages) { (value) in
|
||||
|
||||
// scrolling only user message...
|
||||
|
||||
//if value.last!.myMsg{}//
|
||||
withAnimation {
|
||||
reader.scrollTo(value.last?.id)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.padding([.horizontal,.bottom])
|
||||
.padding(.top, 25)
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
// since bottom edge is ignored....
|
||||
.padding(.bottom,getSafeAreaInsets().bottom)// + keyboard.keyboardHeight
|
||||
.background((self.isLoading || errorMessage?.isEmpty==false)
|
||||
?
|
||||
AnyView(Color.clear.clipped())
|
||||
:
|
||||
AnyView(Color.white.clipShape(RoundedShape())))
|
||||
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.edgesIgnoringSafeArea(.bottom)//
|
||||
//.background(Color("bg").ignoresSafeArea(.all, edges: .all))
|
||||
// .ignoresSafeArea(.all, edges: .top)
|
||||
.background(
|
||||
LinearGradient(colors: [
|
||||
|
||||
Color("BG1"),
|
||||
Color("BG1"),
|
||||
Color("BG2"),
|
||||
Color("BG2"),
|
||||
|
||||
], startPoint: .top, endPoint: .bottom).ignoresSafeArea(.all, edges: .all))
|
||||
.onAppear(){
|
||||
Task{
|
||||
await getPlanList()
|
||||
}
|
||||
}
|
||||
})
|
||||
.navigationBarHidden(true)
|
||||
.navigationBarBackButtonHidden(true) // 隐藏返回按钮
|
||||
.alertBinding($alert)
|
||||
.overlay(
|
||||
GeometryReader { geometry in
|
||||
VStack {
|
||||
|
||||
Spacer() // Pus
|
||||
Group {
|
||||
if isMessageInputBarPresented {
|
||||
MessageInputBar(isPresented: $isMessageInputBarPresented, message: $message,isSendingMsg: $isSendingMsg) { message in
|
||||
print("发送消息: \(message)")
|
||||
|
||||
Task{
|
||||
await SendMsg(msg:message)
|
||||
}
|
||||
}//.shadow(radius: 5)
|
||||
//.padding(.bottom, keyboard.keyboardHeight) // Adjust for keyboard height
|
||||
//.animation(.easeIn(duration: 0.3), value: keyboard.keyboardHeight) // Smooth transition
|
||||
|
||||
}
|
||||
}//.edgesIgnoringSafeArea(.bottom)
|
||||
}.frame(height: geometry.size.height * 1 / 2) // 只占据底部 1/2 的高度
|
||||
.offset(y: geometry.size.height / 2) // 向下移动 1/2
|
||||
}
|
||||
)
|
||||
//.background(Color("Color").edgesIgnoringSafeArea(.top))
|
||||
// Full Screen Image Picker...
|
||||
.fullScreenCover(isPresented: self.$imagePicker, onDismiss: {
|
||||
|
||||
// when ever image picker closes...
|
||||
// verifying if image is selected or cancelled...
|
||||
|
||||
if self.imgData.count != 0{
|
||||
|
||||
//allMessages.writeMessage(id: Date(), msg: "", photo: self.imgData, myMsg: true, profilePic: "p1")
|
||||
}
|
||||
|
||||
}) {
|
||||
|
||||
ImagePicker(imagePicker: self.$imagePicker, imgData: self.$imgData)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct MessageInputBar: View {
|
||||
@Binding var isPresented: Bool
|
||||
@Binding var message:String
|
||||
@Binding var isSendingMsg: Bool
|
||||
|
||||
@ObservedObject private var keyboard = KeyboardResponder()
|
||||
var onSend: (String) -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 1){
|
||||
|
||||
HStack(spacing: 15){
|
||||
|
||||
//TextField("输入消息", text: self.$message).foregroundColor(.black)
|
||||
|
||||
CustomTF(sfIcon: "message", hint: "输入消息", hasDriveLine:false, value: $message).foregroundColor(.black)
|
||||
|
||||
/* Button(action: {
|
||||
|
||||
// toogling image picker...
|
||||
|
||||
imagePicker.toggle()
|
||||
|
||||
}, label: {
|
||||
|
||||
Image(systemName: "paperclip.circle.fill")
|
||||
.font(.system(size: 22))
|
||||
.foregroundColor(.gray)
|
||||
})*/
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal)
|
||||
.background(Color.black.opacity(0.06))
|
||||
.clipShape(Capsule())
|
||||
//.animation(.easeInOut(duration: 0.3)) // 添加动画效果
|
||||
|
||||
// Send Button...
|
||||
|
||||
// hiding view...
|
||||
|
||||
if message != "" {
|
||||
if isSendingMsg == false {
|
||||
Button(action: {
|
||||
onSend(message)
|
||||
// appeding message...
|
||||
|
||||
|
||||
}, label: {
|
||||
|
||||
Image(systemName: "paperplane.fill")
|
||||
.font(.system(size: 22))
|
||||
.foregroundColor(Color("Main"))
|
||||
// rotating the image...
|
||||
.rotationEffect(.init(degrees: 45))
|
||||
// adjusting padding shape...
|
||||
.padding(.vertical,12)
|
||||
.padding(.leading,12)
|
||||
.padding(.trailing,17)
|
||||
.background(Color.black.opacity(0.07))
|
||||
.clipShape(Circle())
|
||||
})
|
||||
}else{
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle()).tint(Color("Main"))
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom)
|
||||
.background(Color.white)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func getPlanList() async {
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/ticket/fetch?id=\(ticketID)")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
|
||||
self.isLoading = false
|
||||
|
||||
if let error = error {
|
||||
self.errorMessage = "数据请求失败: \(error.localizedDescription)"
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
self.errorMessage = "数据请求失败"
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the user info response
|
||||
if let Subscribe = try? JSONDecoder().decode(TicketChatReponse.self, from: data), let data = Subscribe.data {
|
||||
tickchatData = data
|
||||
if let message = Subscribe.data?.message {
|
||||
alltickets = message
|
||||
alltickets.forEach { msg in
|
||||
allMessages.messages.append(msg)
|
||||
}
|
||||
|
||||
if (alltickets.count > 0 && tickchatData?.status == 0){
|
||||
isMessageInputBarPresented.toggle()
|
||||
}
|
||||
}else{
|
||||
self.errorMessage = Subscribe.message ?? ""
|
||||
}
|
||||
|
||||
}else{
|
||||
self.errorMessage = "数据请求失败: JSON 解析错误"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
|
||||
|
||||
public func closeThisTicket() async {
|
||||
|
||||
|
||||
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/ticket/close?id=\(ticketID)")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
|
||||
|
||||
if let error = error {
|
||||
self.alert = Alert(errorMessage: "数据请求失败: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
self.alert = Alert(errorMessage: "数据请求失败")
|
||||
return
|
||||
}
|
||||
// MARK: - TicketSendMsgReponse
|
||||
struct TicketSendMsgReponse: Codable {
|
||||
let status, message: String?
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Parse the user info response
|
||||
if let SendMsgReponse = try? JSONDecoder().decode(TicketSendMsgReponse.self, from: data), let message = SendMsgReponse.message {
|
||||
|
||||
if let stutas = SendMsgReponse.status , stutas == "success" {
|
||||
self.alert = Alert(okMessage: message)
|
||||
}else{
|
||||
self.alert = Alert(errorMessage: message)
|
||||
|
||||
}
|
||||
|
||||
}else{
|
||||
self.alert = Alert(errorMessage: "数据请求失败: JSON 解析错误")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
|
||||
|
||||
public func SendMsg(msg: String) async {
|
||||
|
||||
isSendingMsg = true
|
||||
|
||||
if let encodedMsg = msg.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/ticket/reply?id=\(ticketID)&message=\(encodedMsg)"){
|
||||
|
||||
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
self.isSendingMsg = false
|
||||
|
||||
if let error = error {
|
||||
self.alert = Alert(errorMessage: "数据请求失败: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
self.alert = Alert(errorMessage: "数据请求失败")
|
||||
return
|
||||
}
|
||||
// MARK: - TicketSendMsgReponse
|
||||
struct TicketSendMsgReponse: Codable {
|
||||
let status, message: String?
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Parse the user info response
|
||||
if let SendMsgReponse = try? JSONDecoder().decode(TicketSendMsgReponse.self, from: data), let message = SendMsgReponse.message {
|
||||
let randomInt = Int.random(in: 100...1000)
|
||||
if let stutas = SendMsgReponse.status , stutas == "success" {
|
||||
withAnimation(.easeIn){
|
||||
allMessages.writeMessage(idold: randomInt, msg: msg, myMsg: true, profilePic: "p1")
|
||||
}
|
||||
self.message = ""
|
||||
}else{
|
||||
self.alert = Alert(errorMessage: message)
|
||||
|
||||
}
|
||||
|
||||
}else{
|
||||
self.alert = Alert(errorMessage: "数据请求失败: JSON 解析错误")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Chat Bubbles...
|
||||
|
||||
struct ChatBubble : View {
|
||||
|
||||
var msg : Message
|
||||
|
||||
var body: some View{
|
||||
|
||||
// Automatic scroll To Bottom...
|
||||
// First Assigning Id To Each Row...
|
||||
|
||||
HStack(alignment: .top,spacing: 10){
|
||||
|
||||
if msg.isMe{
|
||||
|
||||
// pushing msg to left...
|
||||
|
||||
// minimum space ...
|
||||
|
||||
// Modifying for Image...
|
||||
|
||||
Spacer(minLength: 25)
|
||||
|
||||
|
||||
if msg.photo == nil{
|
||||
|
||||
Text("\(msg.message)")
|
||||
.padding(.all)
|
||||
.background(Color.black.opacity(0.06))
|
||||
.clipShape(BubbleArrow(myMsg: msg.isMe)).foregroundColor(.black)
|
||||
|
||||
|
||||
|
||||
}
|
||||
else{
|
||||
|
||||
// Image(uiImage: UIImage(data: msg.photo!)!).resizable()
|
||||
AsyncImage(url: URL(string: msg.photo ?? ""))
|
||||
.frame(width: UIScreen.main.bounds.width - 150, height: 150)
|
||||
.clipShape(BubbleArrow(myMsg: msg.isMe))
|
||||
}
|
||||
|
||||
// profile Image...
|
||||
|
||||
Image(msg.profilePic ?? "p1" )
|
||||
.resizable()
|
||||
.frame(width: 30, height: 30)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
else{
|
||||
|
||||
// pushing msg to right...
|
||||
|
||||
// profile Image...
|
||||
|
||||
Image(msg.profilePic ?? "p2")
|
||||
.resizable()
|
||||
.frame(width: 30, height: 30)
|
||||
.clipShape(Circle())
|
||||
|
||||
if msg.photo == nil{
|
||||
Text(msg.message)
|
||||
.foregroundColor(.white)
|
||||
.padding(.all)
|
||||
.background(Color("Main"))
|
||||
.clipShape(BubbleArrow(myMsg: msg.isMe))
|
||||
|
||||
|
||||
// Text("回复时间:\(TimestampConverter.convertTimestampToDateString(msg.updatedAt)).font(.caption).foregroundColor(.black.opacity(0.8))")
|
||||
|
||||
}
|
||||
else{
|
||||
|
||||
// Image(uiImage: UIImage(data: msg.photo!)!)
|
||||
// .resizable()
|
||||
AsyncImage(url: URL(string: msg.photo ?? ""))
|
||||
.frame(width: UIScreen.main.bounds.width - 150, height: 150)
|
||||
.clipShape(BubbleArrow(myMsg: msg.isMe))
|
||||
}
|
||||
|
||||
|
||||
Spacer(minLength: 25)
|
||||
}
|
||||
}
|
||||
.id(msg.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Bubble Arrow...
|
||||
|
||||
struct BubbleArrow : Shape {
|
||||
|
||||
var myMsg : Bool
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
|
||||
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: myMsg ? [.topLeft,.bottomLeft,.bottomRight] : [.topRight,.bottomLeft,.bottomRight], cornerRadii: CGSize(width: 10, height: 10))
|
||||
|
||||
return Path(path.cgPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Rounded Shape...
|
||||
|
||||
struct RoundedShape : Shape {
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
|
||||
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: [.topLeft,.topRight], cornerRadii: CGSize(width: 35, height: 35))
|
||||
|
||||
return Path(path.cgPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Model Data For Message...
|
||||
|
||||
/*struct Message : Identifiable,Equatable{
|
||||
|
||||
var id : Date
|
||||
var message : String
|
||||
var myMsg : Bool
|
||||
var profilePic : String
|
||||
var photo: Data?
|
||||
|
||||
}*/
|
||||
|
||||
class Messages : ObservableObject{
|
||||
|
||||
@Published var messages : [Message] = []
|
||||
|
||||
// sample data...
|
||||
|
||||
init() {
|
||||
|
||||
/*let strings = ["Hii","Hello !!!!","What's Up, What Are You Doing ???","Nothing Just Simply Enjoying Quarintine Holidays..You???","Same :))","Ohhhhh","What About Your Country ???","Very Very Bad...","Ok Be Safe","Bye....","Ok...."]
|
||||
|
||||
//simple logic for two side messages
|
||||
|
||||
for i in 0..<strings.count{
|
||||
|
||||
messages.append(Message(id: Date(), message: strings[i], myMsg: i % 2 == 0 ? true : false, profilePic: i % 2 == 0 ? "p1" : "p2"))
|
||||
}*/
|
||||
}
|
||||
|
||||
func writeMessage(idold: Int,msg: String,myMsg: Bool,profilePic: String?){
|
||||
messages.append(Message(idold: idold, ticketID: 0, isMe: myMsg, message: msg, photo: nil, createdAt: Int(Date().timeIntervalSince1970), updatedAt: Int(Date().timeIntervalSince1970), profilePic: profilePic))
|
||||
//messages.append(Message(id: id, message: msg, myMsg: myMsg, profilePic: profilePic, photo: photo))
|
||||
}
|
||||
}
|
||||
|
||||
// Image Picker...
|
||||
|
||||
struct ImagePicker : UIViewControllerRepresentable {
|
||||
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
|
||||
return ImagePicker.Coordinator(parent1: self)
|
||||
}
|
||||
|
||||
@Binding var imagePicker : Bool
|
||||
@Binding var imgData : Data
|
||||
|
||||
func makeUIViewController(context: Context) -> UIImagePickerController{
|
||||
|
||||
let picker = UIImagePickerController()
|
||||
picker.sourceType = .photoLibrary
|
||||
picker.delegate = context.coordinator
|
||||
return picker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
|
||||
|
||||
}
|
||||
|
||||
class Coordinator : NSObject,UIImagePickerControllerDelegate,UINavigationControllerDelegate{
|
||||
|
||||
var parent : ImagePicker
|
||||
|
||||
init(parent1 : ImagePicker) {
|
||||
|
||||
parent = parent1
|
||||
}
|
||||
|
||||
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
|
||||
parent.imagePicker.toggle()
|
||||
}
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||
|
||||
let image = info[.originalImage] as! UIImage
|
||||
parent.imgData = image.jpegData(compressionQuality: 0.5)!
|
||||
parent.imagePicker.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,530 @@
|
||||
//
|
||||
// SupportticketView.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import ApplicationLibrary
|
||||
|
||||
|
||||
|
||||
struct SupportTicketView : View {
|
||||
|
||||
var edges = getSafeAreaInsets()
|
||||
|
||||
@State var selectedTab = "Chats"
|
||||
@Namespace var animation
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode // 环境变量用于控制视图的呈现
|
||||
@State private var TicketsList: [TicketsReponseDatum] = []
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
@State private var alert: Alert?
|
||||
|
||||
@State private var showInputSheet = false
|
||||
@State private var tickettitle = ""
|
||||
@State private var userInput = ""
|
||||
|
||||
@State private var isInputLoading = false
|
||||
@State private var submitmessage = ""
|
||||
var body: some View{
|
||||
NavigationView(content: {
|
||||
VStack(spacing: 0){
|
||||
|
||||
VStack{
|
||||
|
||||
ZStack{
|
||||
|
||||
HStack{
|
||||
Button(action: {
|
||||
|
||||
presentationMode.wrappedValue.dismiss() // 手动触发返回
|
||||
}, label: {
|
||||
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("返回")
|
||||
.foregroundColor(.white)
|
||||
}).padding(10) // 增加可点击区域
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
Button(action: {
|
||||
showInputSheet.toggle()
|
||||
}, label: {
|
||||
// Image(systemName: "note.text.badge.plus")
|
||||
Text("提交新工单").foregroundColor(.red)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
Text("我的工单").fontWeight(.bold).lineLimit(1)
|
||||
|
||||
}
|
||||
.padding()
|
||||
// .padding(.top,edges.top)
|
||||
.foregroundColor(.white)
|
||||
|
||||
|
||||
// HStack(spacing: 20){
|
||||
//
|
||||
// ForEach(tabs,id: \.self){title in
|
||||
//
|
||||
// TabButton(selectedTab: $selectedTab, title: title, animation: animation)
|
||||
// }
|
||||
// }
|
||||
// .padding()
|
||||
// .background(Color.white.opacity(0.08))
|
||||
// .cornerRadius(15)
|
||||
// .padding(.vertical)
|
||||
}
|
||||
.padding(.bottom)
|
||||
// .background(Color("top"))
|
||||
// .clipShape(CustomCorner(corner: .bottomLeft, size: 65))
|
||||
|
||||
ZStack{
|
||||
|
||||
// Color("top")
|
||||
|
||||
// Color("bg")
|
||||
// .clipShape(CustomCorner(corner: .topRight, size: 65))
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false, content: {
|
||||
|
||||
VStack(spacing: 20){
|
||||
|
||||
/*HStack{
|
||||
|
||||
Text("All Chats")
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
Button(action: {}, label: {
|
||||
|
||||
Image(systemName: "slider.horizontal.3")
|
||||
.font(.system(size: 22))
|
||||
.foregroundColor(.primary)
|
||||
})
|
||||
}
|
||||
.padding([.horizontal,.top])*/
|
||||
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
.padding()
|
||||
}
|
||||
|
||||
if let errorMessage = errorMessage {
|
||||
Text(errorMessage)
|
||||
.bold()
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center) // 确保多行文本居中
|
||||
.padding()
|
||||
}
|
||||
if TicketsList.count > 0 {
|
||||
|
||||
ForEach(TicketsList){chatData in
|
||||
// Chat View...
|
||||
NavigationLink(destination: SupportTicketChatView(ticketID: chatData.idOLD)) {
|
||||
ChatView(chatData: chatData)
|
||||
}
|
||||
}
|
||||
}else{
|
||||
if !isLoading
|
||||
{
|
||||
Text("暂无记录").font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.multilineTextAlignment(.center) // 确保多行文本居中
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical)
|
||||
|
||||
})
|
||||
//.clipShape(CustomCorner(corner: .topRight, size: 65))
|
||||
// its cutting off inside view may be its a bug....
|
||||
}
|
||||
}.onAppear(){
|
||||
Task{
|
||||
await getPlanList()
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
//.background(Color("bg").ignoresSafeArea(.all, edges: .all))
|
||||
.background(
|
||||
LinearGradient(colors: [
|
||||
|
||||
Color("BG1"),
|
||||
Color("BG1"),
|
||||
Color("BG2"),
|
||||
Color("BG2"),
|
||||
|
||||
], startPoint: .top, endPoint: .bottom).ignoresSafeArea(.all, edges: .all))
|
||||
// .ignoresSafeArea(.all, edges: .top)
|
||||
.sheet(isPresented: $showInputSheet) {
|
||||
InputSheet(tickettitle: $tickettitle, userInput: $userInput, isInputLoading: $isInputLoading, submitmessage: $submitmessage, onSubmit: {
|
||||
Task {
|
||||
// 提交网络请求
|
||||
await submitTicketData(title: tickettitle, userInput: userInput)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
.navigationBarHidden(true)
|
||||
.alertBinding($alert)
|
||||
.navigationBarBackButtonHidden(true) // 隐藏返回按钮
|
||||
}
|
||||
|
||||
public func submitTicketData(title: String, userInput: String) async{
|
||||
isInputLoading = true
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/ticket/save?subject=\(title)&level=0&message=\(userInput)")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
isInputLoading = false
|
||||
if let error = error {
|
||||
submitmessage = ( "数据请求失败: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
submitmessage = ( "数据请求失败")
|
||||
return
|
||||
}
|
||||
// MARK: - TicketSendMsgReponse
|
||||
struct TicketSendMsgReponse: Codable {
|
||||
let status, message: String?
|
||||
|
||||
}
|
||||
|
||||
// Parse the user info response
|
||||
if let SendMsgReponse = try? JSONDecoder().decode(TicketSendMsgReponse.self, from: data), let message = SendMsgReponse.message {
|
||||
|
||||
if let stutas = SendMsgReponse.status , stutas == "success" {
|
||||
submitmessage = message
|
||||
//1s 后 关闭窗口
|
||||
withAnimation {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
showInputSheet = false
|
||||
//刷新界面
|
||||
Task{
|
||||
await getPlanList()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}else{
|
||||
submitmessage = (message)
|
||||
}
|
||||
|
||||
}else{
|
||||
submitmessage = ( "数据请求失败: JSON 解析错误")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
|
||||
|
||||
public func getPlanList() async {
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/ticket/fetch")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
|
||||
if let error = error {
|
||||
self.errorMessage = "数据请求失败: \(error.localizedDescription)"
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
self.errorMessage = "数据请求失败"
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the user info response
|
||||
if let Subscribe = try? JSONDecoder().decode(TicketsReponse.self, from: data) {
|
||||
|
||||
if let data = Subscribe.data {
|
||||
TicketsList = data
|
||||
}else{
|
||||
self.errorMessage = Subscribe.message ?? ""
|
||||
}
|
||||
|
||||
}else{
|
||||
self.errorMessage = "数据请求失败: JSON 解析错误"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
var tabs = ["Chats","Status","Calls"]
|
||||
|
||||
struct TabButton : View {
|
||||
|
||||
@Binding var selectedTab : String
|
||||
var title : String
|
||||
var animation : Namespace.ID
|
||||
|
||||
var body: some View{
|
||||
|
||||
Button(action: {
|
||||
|
||||
withAnimation{
|
||||
|
||||
selectedTab = title
|
||||
}
|
||||
|
||||
}, label: {
|
||||
|
||||
Text(title)
|
||||
.foregroundColor(.white)
|
||||
.padding(.vertical,10)
|
||||
.padding(.horizontal)
|
||||
// Sliding Effect...
|
||||
.background(
|
||||
|
||||
ZStack{
|
||||
|
||||
if selectedTab == title{
|
||||
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color("top"))
|
||||
.matchedGeometryEffect(id: "Tab", in: animation)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomCorner : Shape {
|
||||
|
||||
var corner : UIRectCorner
|
||||
var size : CGFloat
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
|
||||
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corner, cornerRadii: CGSize(width: size, height: size))
|
||||
|
||||
return Path(path.cgPath)
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatView : View {
|
||||
|
||||
var chatData : TicketsReponseDatum
|
||||
|
||||
var body: some View{
|
||||
|
||||
HStack(spacing: 10){
|
||||
VStack(spacing: 12) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 8, content: {
|
||||
|
||||
Text(chatData.subject)
|
||||
.fontWeight(.bold)
|
||||
.lineLimit(1).foregroundColor(.white)
|
||||
|
||||
// Text("官方回复:\(chatData.message ?? "未回复")")
|
||||
// .font(.caption)
|
||||
// .lineLimit(1)
|
||||
|
||||
HStack(content: {
|
||||
|
||||
if(chatData.status == 1){
|
||||
Text("工单状态 : 已关闭").fontWeight(.light).foregroundColor(.gray).font(.subheadline)
|
||||
}else if(chatData.status == 0){
|
||||
Text("工单状态 : 待回复").fontWeight(.light).foregroundColor(.red).font(.subheadline)
|
||||
}else if(chatData.status == 2){
|
||||
Text("工单状态 : 已回复").fontWeight(.light).foregroundColor(.green).font(.subheadline)
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
Text("\(TimestampConverter.convertTimestampToDateString(chatData.updatedAt))")
|
||||
.font(.subheadline).foregroundColor(.white)
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
// Model And Sample Data....
|
||||
//
|
||||
//struct Chat : Identifiable {
|
||||
//
|
||||
// var id = UUID().uuidString
|
||||
// var name : String
|
||||
// var image : String
|
||||
// var msg : String
|
||||
// var time : String
|
||||
//}
|
||||
//
|
||||
//// were going to do custom grouping of views....
|
||||
//
|
||||
//struct HomeData {
|
||||
//
|
||||
// var groupName : String
|
||||
// var groupData : [Chat]
|
||||
//}
|
||||
|
||||
//var FriendsChat : [Chat] = [
|
||||
//
|
||||
// Chat(name: "iJustine",image: "p0", msg: "Hey EveryOne !!!", time: "02:45"),
|
||||
// Chat(name: "Kavsoft",image: "p1", msg: "Learn - Develop - Deploy", time: "03:45"),
|
||||
// Chat(name: "SwiftUI",image: "p2", msg: "New Framework For iOS", time: "04:55"),
|
||||
// Chat(name: "Bill Gates",image: "p3", msg: "Founder Of Microsoft", time: "06:25"),
|
||||
// Chat(name: "Tim Cook",image: "p4", msg: "Apple lnc CEO", time: "07:19"),
|
||||
// Chat(name: "Jeff",image: "p5", msg: "I dont Know How To Spend Money :)))", time: "08:22"),
|
||||
//]
|
||||
//
|
||||
//var GroupChat : [Chat] = [
|
||||
//
|
||||
// Chat(name: "iTeam",image: "p0", msg: "Hey EveryOne !!!", time: "02:45"),
|
||||
// Chat(name: "Kavsoft - Developers",image: "p1", msg: "Next Video :))))", time: "03:45"),
|
||||
// Chat(name: "SwiftUI - Community",image: "p2", msg: "New File Importer/Exporter", time: "04:55"),
|
||||
//]
|
||||
//
|
||||
//var data = [
|
||||
//
|
||||
// // Group 1
|
||||
// HomeData(groupName: "", groupData: FriendsChat),
|
||||
//]
|
||||
|
||||
struct InputSheet: View {
|
||||
@Binding var tickettitle: String
|
||||
@Binding var userInput: String
|
||||
@Binding var isInputLoading: Bool
|
||||
@Binding var submitmessage:String
|
||||
var onSubmit: () -> Void
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("新的工单").fontWeight(.bold).lineLimit(1)
|
||||
|
||||
Text("请输入工单标题").font(.subheadline) .foregroundColor(.gray)
|
||||
.frame(maxWidth: .infinity)
|
||||
.multilineTextAlignment(.leading)
|
||||
.padding(.leading, 5) // 添加左侧内边距
|
||||
.padding(.top, 8) // 添加顶部内边距
|
||||
|
||||
TextEditor( text: $tickettitle)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.padding()
|
||||
.frame(width: UIScreen.main.bounds.width * 0.9,height: 50)
|
||||
.background(Color.clear) // 设置背景为透明
|
||||
.cornerRadius(10) // 圆角
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(Color.gray.opacity(0.5), lineWidth: 1)
|
||||
) // 添加边框
|
||||
.background(Color.clear) // 设置背景为透明// 添加边框
|
||||
.font(.system(size: 18))
|
||||
.transparentScrolling()
|
||||
|
||||
|
||||
|
||||
Text("请输入工单内容").font(.subheadline) .foregroundColor(.gray)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.leading, 5) // 添加左侧内边距
|
||||
.padding(.top, 8) // 添加顶部内边距
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
TextEditor(text: $userInput)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.padding()
|
||||
.frame(width: UIScreen.main.bounds.width * 0.9,height: UIScreen.main.bounds.height * 0.4)
|
||||
.background(Color.clear) // 设置背景为透明
|
||||
.cornerRadius(10) // 圆角
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(Color.gray.opacity(0.5), lineWidth: 1)
|
||||
) // 添加边框
|
||||
.font(.system(size: 18))
|
||||
.transparentScrolling()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
if !submitmessage.isEmpty{
|
||||
Text(submitmessage).font(.subheadline).foregroundColor(Color.red.opacity(0.9))
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
if( tickettitle.count > 0 && userInput.count > 0){
|
||||
onSubmit() // 提交时触发回调
|
||||
}
|
||||
|
||||
}) {
|
||||
if isInputLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle()).tint(Color("Main"))
|
||||
} else {
|
||||
Text("提交工单")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.bottom, 20)
|
||||
|
||||
|
||||
}
|
||||
.background(Color("bg").ignoresSafeArea(.all, edges: .all))
|
||||
.ignoresSafeArea(edges: .bottom) // 忽略安全区域,保证按钮位置靠底部
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// SupportView.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Crisp
|
||||
|
||||
struct SupportView: View {
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
var body: some View {
|
||||
NavigationView(content: {
|
||||
VStack{
|
||||
ZStack{
|
||||
|
||||
HStack{
|
||||
Button(action: {
|
||||
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}, label: {
|
||||
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("返回")
|
||||
.foregroundColor(.white)
|
||||
}).padding(10)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
}
|
||||
|
||||
Text("客服(技术支持)")
|
||||
.fontWeight(.bold).lineLimit(1)
|
||||
}
|
||||
.padding()
|
||||
// .padding(.top,edges.top)
|
||||
.foregroundColor(.white)
|
||||
ChatViewControllerWrapper()
|
||||
// SomeUIElement()
|
||||
// WebView(url: URL(string: UserManager.shared.kefuUrl())!)
|
||||
|
||||
}.navigationBarHidden(true).edgesIgnoringSafeArea(.bottom)
|
||||
.background(
|
||||
LinearGradient(colors: [
|
||||
|
||||
Color("BG1"),
|
||||
Color("BG1"),
|
||||
Color("BG2"),
|
||||
Color("BG2"),
|
||||
|
||||
], startPoint: .top, endPoint: .bottom)
|
||||
)
|
||||
})
|
||||
|
||||
.navigationBarHidden(true)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// WebView.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/12.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
|
||||
struct WebView: UIViewRepresentable {
|
||||
|
||||
var url: URL
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
return WKWebView()
|
||||
}
|
||||
|
||||
func updateUIView(_ webView: WKWebView, context: Context) {
|
||||
let request = URLRequest(url: url)
|
||||
webView.load(request)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct WebViewContent: UIViewRepresentable {
|
||||
|
||||
var content: String
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
return WKWebView()
|
||||
}
|
||||
|
||||
func updateUIView(_ webView: WKWebView, context: Context) {
|
||||
// let request = URLRequest(url: url)
|
||||
// webView.load(request)
|
||||
webView.loadHTMLString(content, baseURL: nil)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user