Add CI/CD configuration and API documentation
This commit is contained in:
@@ -0,0 +1,477 @@
|
||||
//
|
||||
// SubscriptionView.swift
|
||||
// SFI
|
||||
//
|
||||
// Created by Mac on 2024/10/12.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct SubscriptionView: View {
|
||||
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
@State private var planList: [DatuPlanResponse] = []
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
@State private var alert: Alert?
|
||||
@State var orderinfo : OrderInfoDatum?
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
let adimages = ["10-35-24", "10-35-15", "10-35-03"] // 替换为你广告图片的名字
|
||||
let adimagesString = ["UUVPN 采用最高安全 ECC 加密技术,仅需一键操作,军事级的加密技术将护航您的任何互联网访问", "UUVPN 拥有170 个热门城市的300+高速服务器,您可以随时随地获得高速安全的互联网体验", "UUVPN 支持所有当前主流的平台系统,您可以在几乎任何设备上使用"]
|
||||
@Environment(\.presentationMode) var presentationMode // 环境变量用于控制视图的呈现
|
||||
// 当前页的索引
|
||||
|
||||
|
||||
@State private var isNavigationActive = false // 控制跳转
|
||||
|
||||
@State private var currentIndex = 0
|
||||
// 自动轮播计时器
|
||||
let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
|
||||
|
||||
var body: some View {
|
||||
NavigationView(content: {
|
||||
VStack {
|
||||
ZStack{
|
||||
|
||||
|
||||
HStack{
|
||||
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
isPresented = false // Close the side menu
|
||||
}
|
||||
presentationMode.wrappedValue.dismiss() // 手动触发返回
|
||||
}, label: {
|
||||
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("返回")
|
||||
.foregroundColor(.white)
|
||||
}).padding(10) // 增加可点击区域
|
||||
|
||||
Spacer()
|
||||
|
||||
NavigationLink(destination: OrderListView()) {
|
||||
Text("我的订单").foregroundColor(.red)
|
||||
}.listRowBackground(Color.clear)
|
||||
|
||||
}
|
||||
Spacer()
|
||||
VStack(spacing: 5){
|
||||
|
||||
Text( "升级套餐")
|
||||
.fontWeight(.bold).lineLimit(1).frame(width: UIScreen.main.bounds.width*0.5)
|
||||
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
|
||||
}
|
||||
.padding(.all)
|
||||
|
||||
if let orderinfoS = orderinfo{
|
||||
|
||||
NavigationLink(destination: OrderPaymentView(orderinfo: orderinfoS),isActive: $isNavigationActive, label: {
|
||||
EmptyView() // 隐藏的视图
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
//
|
||||
// // Subscription Plans
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
// VPN Info and Trial Offer
|
||||
VStack(spacing: 8) {
|
||||
// 图片轮播
|
||||
TabView(selection: $currentIndex) {
|
||||
ForEach(0..<adimages.count, id: \.self) { index in
|
||||
VStack(spacing: 8) {
|
||||
Image(adimages[index])
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: UIScreen.main.bounds.width * 0.8,height: 150)
|
||||
.cornerRadius(10)
|
||||
.tag(index)
|
||||
|
||||
|
||||
Text(adimagesString[index])
|
||||
.font(.subheadline)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.gray)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) // 隐藏系统自带的指示器
|
||||
.frame(height: 220)
|
||||
.onReceive(timer) { _ in
|
||||
// 自动切换广告
|
||||
withAnimation {
|
||||
currentIndex = (currentIndex + 1) % adimages.count
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义指示器
|
||||
HStack(spacing: 8) {
|
||||
ForEach(0..<adimages.count, id: \.self) { index in
|
||||
Circle()
|
||||
.fill(index == currentIndex ? Color.green : Color.gray)
|
||||
.frame(width: 5, height: 5)
|
||||
}
|
||||
}
|
||||
// .padding(.top, 3)
|
||||
|
||||
|
||||
// Free Trial Text
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "checkmark.shield.fill")
|
||||
.foregroundColor(.green)
|
||||
Text("首次订阅免费试用 2 天")
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
|
||||
}
|
||||
.padding(.top, 10)
|
||||
|
||||
Text("订阅可随时取消,试用期内取消不收取任何费用")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.padding(.top, 20)
|
||||
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
.padding()
|
||||
}
|
||||
|
||||
if let errorMessage = errorMessage {
|
||||
Text(errorMessage)
|
||||
.bold()
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center) // 确保多行文本居中
|
||||
.padding()
|
||||
}
|
||||
VStack(spacing: 16) {
|
||||
if planList.isEmpty {
|
||||
// Text("Loading plans...") // Loading indicator
|
||||
} else {
|
||||
|
||||
ForEach(planList) { plan in
|
||||
|
||||
Button(action: {
|
||||
//print( plan.content ?? "")
|
||||
Task{
|
||||
await subPlanOrder(plan_id:plan.idOLD ?? 0,amount : plan.monthPrice ?? 0)
|
||||
}
|
||||
}, label: {
|
||||
|
||||
SubscriptionPlanView(planName: plan.name, originalPrice: "¥\(String(format: "%.2f", Double(plan.monthPrice ?? 0)/100)) /月", discountedPrice: "", discountPercentage: "", monthlyPrice: "", content: plan.content ?? "", bestPlan: plan.idOLD == planList.count,planContent: "\(plan.transferEnable ?? 0)GB/月")
|
||||
|
||||
// Text( plan.content ?? "").padding()
|
||||
|
||||
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}.padding(.top, 20)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Subscription Notice
|
||||
Text("订阅须知:仅支持微信和支付宝付款")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
.background(
|
||||
LinearGradient(colors: [
|
||||
|
||||
Color("BG1"),
|
||||
Color("BG1"),
|
||||
Color("BG2"),
|
||||
Color("BG2"),
|
||||
|
||||
], startPoint: .top, endPoint: .bottom)
|
||||
)
|
||||
// .background(.white)
|
||||
.onAppear(){
|
||||
Task{
|
||||
await getPlanList()
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
|
||||
})
|
||||
.navigationBarHidden(true)
|
||||
.alertBinding($alert)
|
||||
.navigationBarBackButtonHidden(true) // 隐藏返回按钮
|
||||
}
|
||||
|
||||
|
||||
|
||||
public func subPlanOrder(plan_id: Int,amount: Int) async {
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/order/save?plan_id=\(plan_id)&period=month_price&coupon_code=")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
|
||||
if let error = error {
|
||||
alert = Alert(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
alert = Alert(errorMessage: "提交订单信息错误,请重试")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
struct saveOrderResponse: Codable {
|
||||
let status, message, data: String?
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Parse the user info response
|
||||
if let message = try? JSONDecoder().decode(saveOrderResponse.self, from: data) {
|
||||
if let dataNO = message.data,dataNO.count > 10 {
|
||||
Task{
|
||||
await getOrderInfo(OrderNO: dataNO)
|
||||
}
|
||||
|
||||
|
||||
}else{
|
||||
alert = Alert(errorMessage: message.message ?? "" )
|
||||
}
|
||||
|
||||
//2024101311320180720&sitename=
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
|
||||
|
||||
}
|
||||
|
||||
func getOrderInfo(OrderNO: String) async {
|
||||
|
||||
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/order/detail?trade_no=\(OrderNO)")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
|
||||
if let error = error {
|
||||
self.errorMessage = "数据请求失败: \(error.localizedDescription)"
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
self.errorMessage = "数据请求失败"
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the user info response
|
||||
if let Subscribe = try? JSONDecoder().decode(OrderInfoSingleReponse.self, from: data) {
|
||||
|
||||
if let data = Subscribe.data {
|
||||
orderinfo = data
|
||||
isNavigationActive = true
|
||||
}else{
|
||||
self.errorMessage = Subscribe.message ?? ""
|
||||
}
|
||||
|
||||
}else{
|
||||
self.errorMessage = "数据请求失败: JSON 解析错误"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
func getPlanList() async {
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
|
||||
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/plan/fetch")!
|
||||
var request = URLRequest(url: userInfoUrl)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
|
||||
if let error = error {
|
||||
self.errorMessage = "数据请求失败: \(error.localizedDescription)"
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
self.errorMessage = "数据请求失败"
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the user info response
|
||||
if let Subscribe = try? JSONDecoder().decode(PlanResponse.self, from: data) {
|
||||
|
||||
if let data = Subscribe.data {
|
||||
planList = data
|
||||
}else{
|
||||
self.errorMessage = Subscribe.message ?? ""
|
||||
}
|
||||
|
||||
}else{
|
||||
self.errorMessage = "数据请求失败: JSON 解析错误"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct SubscriptionPlanView: View {
|
||||
var planName: String
|
||||
var originalPrice: String
|
||||
var discountedPrice: String
|
||||
var discountPercentage: String
|
||||
var monthlyPrice: String
|
||||
|
||||
var content: String
|
||||
var bestPlan: Bool = false
|
||||
var planContent: String
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack(spacing: 5, content: {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(planName)
|
||||
.font(.headline)
|
||||
.fontWeight(bestPlan ? .bold : .regular)
|
||||
.foregroundColor(bestPlan ? .green : .black)
|
||||
Text(planContent).font(.subheadline).fontWeight(bestPlan ? .bold : .regular).foregroundColor( .black)
|
||||
if bestPlan {
|
||||
Text("Best")
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
.padding(4)
|
||||
.background(Color.red)
|
||||
.cornerRadius(4)
|
||||
.foregroundColor(.white)
|
||||
.offset(x: -15, y:16)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
|
||||
|
||||
HStack {
|
||||
/* if bestPlan {
|
||||
Text("\(discountPercentage) OFF")
|
||||
.font(.caption)
|
||||
.padding(4)
|
||||
.foregroundColor(.white).background(.green)
|
||||
.cornerRadius(16)
|
||||
}else{
|
||||
Text("\(discountPercentage) OFF")
|
||||
.font(.caption)
|
||||
.padding(4)
|
||||
.foregroundColor(.black).background(.gray.opacity(0.5))
|
||||
.cornerRadius(16)
|
||||
}
|
||||
Text(discountedPrice)
|
||||
.strikethrough()
|
||||
.foregroundColor(.gray)
|
||||
|
||||
*/
|
||||
Text(originalPrice)
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.black)
|
||||
|
||||
}
|
||||
|
||||
Text(monthlyPrice)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
// Text(extractChinese(from: content)).font(.subheadline).foregroundStyle(.black).padding()
|
||||
//HTMLTextView(htmlString:content)
|
||||
|
||||
}).padding()
|
||||
|
||||
.background(Color.white.opacity(0.9))
|
||||
.cornerRadius(10)
|
||||
.shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 3)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
||||
func extractChinese(from text: String) -> String {
|
||||
let pattern = "[\\u4e00-\\u9fa5]+" // 匹配中文字符的正则表达式
|
||||
let regex = try? NSRegularExpression(pattern: pattern, options: [])
|
||||
let nsString = text as NSString
|
||||
let results = regex?.matches(in: text, options: [], range: NSRange(location: 0, length: nsString.length))
|
||||
|
||||
// 提取匹配的中文
|
||||
let chineseStrings = results?.compactMap { result -> String? in
|
||||
|
||||
if let range = Range(result.range, in: text) {
|
||||
return String(text[range])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
dump(chineseStrings)
|
||||
return chineseStrings?.joined(separator: " ") ?? ""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//struct SubscriptionView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// SubscriptionView()
|
||||
// }
|
||||
//}
|
||||
Reference in New Issue
Block a user