add swiftUI code
This commit is contained in:
@@ -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
@@ -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:确定您所安装的小熊加速器是最新版本 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 Balaji 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 Balaji 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")
|
||||
await 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 Balaji 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 Balaji 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 Balaji 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 Balaji 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 Balaji 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 Balaji 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("小熊加速器 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, 小熊加速器 版权所有")
|
||||
.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 // Close the side menu
|
||||
}, 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() // Fill the remaining space to push the white rectangle to the left
|
||||
}
|
||||
.frame(height: 20) // Set the total width and height for the bar
|
||||
.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 = ["小熊加速器 采用最高安全 ECC 加密技术,仅需一键操作,军事级的加密技术将护航您的任何互联网访问", "小熊加速器 拥有170 个热门城市的300+高速服务器,您可以随时随地获得高速安全的互联网体验", "小熊加速器 支持所有当前主流的平台系统,您可以在几乎任何设备上使用"]
|
||||
@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