Parcourir la source

修改一些显示问题

afan il y a 5 jours
Parent
révision
b5decd4bf2

+ 1
- 1
LotteryTracker/Extensions/Color+Extensions.swift Voir le fichier

@@ -16,7 +16,7 @@ extension Color {
16 16
         switch type {
17 17
         case .doubleColorBall: return .red
18 18
         case .superLotto: return .blue
19
-        case .welfare3D: return .green
19
+        case .happy8: return .green
20 20
         case .sevenStar: return .purple
21 21
         case .other: return .gray
22 22
         }

+ 1
- 1
LotteryTracker/Models/LotteryTicket+Extensions.swift Voir le fichier

@@ -18,7 +18,7 @@ extension Notification.Name {
18 18
 enum LotteryType: String, CaseIterable, Identifiable {
19 19
     case doubleColorBall = "双色球"
20 20
     case superLotto = "大乐透"
21
-    case welfare3D = "福彩3D"
21
+    case happy8 = "快乐八"
22 22
     case sevenStar = "七星彩"
23 23
     case other = "其他"
24 24
     

+ 2
- 2
LotteryTracker/Services/DemoDataGenerator.swift Voir le fichier

@@ -19,7 +19,7 @@ class DemoDataGenerator {
19 19
     func generateDemoData(count: Int = 50) {
20 20
         clearAllData()
21 21
         
22
-        let types: [LotteryType] = [.doubleColorBall, .superLotto, .welfare3D, .sevenStar]
22
+        let types: [LotteryType] = [.doubleColorBall, .superLotto, .happy8, .sevenStar]
23 23
         let calendar = Calendar.current
24 24
         let now = Date()
25 25
         
@@ -91,7 +91,7 @@ class DemoDataGenerator {
91 91
             let backBalls = (1...12).shuffled().prefix(2).map { String(format: "%02d", $0) }
92 92
             return "\(frontBalls.joined(separator: " ")) + \(backBalls.joined(separator: " "))"
93 93
             
94
-        case .welfare3D:
94
+        case .happy8:
95 95
             let numbers = (0...9).shuffled().prefix(3).map { String($0) }
96 96
             return numbers.joined(separator: " ")
97 97
             

+ 5
- 0
LotteryTracker/Utils/Formatters.swift Voir le fichier

@@ -42,4 +42,9 @@ class Formatters {
42 42
         let components = calendar.dateComponents([.day], from: from, to: to)
43 43
         return components.day ?? 0
44 44
     }
45
+    
46
+    // 格式化双色球
47
+    static func formatDoubleColorBall(_ Numbers: String) -> [Int] {
48
+        return []
49
+    }
45 50
 }

+ 69
- 49
LotteryTracker/ViewModels/AddTicketViewModel.swift Voir le fichier

@@ -14,12 +14,12 @@ class AddTicketViewModel: ObservableObject {
14 14
     @Published var selectedType: LotteryType = .doubleColorBall
15 15
     @Published var numbers: String = ""
16 16
     @Published var betCount: Int = 1
17
-    @Published var amountPerBet: String = "2"
17
+    @Published var amountPerBet: Int = 2
18 18
     
19
-    // 修改:改为购买日期(默认今天)
19
+    // 购买日期(默认今天)
20 20
     @Published var purchaseDate: Date = Date()
21 21
     
22
-    // 修改:开奖日期根据彩票类型自动计算
22
+    // 开奖日期根据彩票类型自动计算
23 23
     var drawDate: Date {
24 24
         calculateDrawDate()
25 25
     }
@@ -30,7 +30,7 @@ class AddTicketViewModel: ObservableObject {
30 30
     
31 31
     // 计算属性
32 32
     var totalAmount: Float {
33
-        let amount = Int(amountPerBet) ?? 0
33
+        let amount = amountPerBet
34 34
         return Float(amount * betCount)
35 35
     }
36 36
     
@@ -65,8 +65,8 @@ class AddTicketViewModel: ObservableObject {
65 65
             // 大乐透:每周一、三、六开奖
66 66
             return nextDrawDate(for: [1, 3, 6], from: purchaseDate)
67 67
             
68
-        case .welfare3D:
69
-            // 福彩3D:每天开奖
68
+        case .happy8:
69
+            // 快乐八:每天开奖
70 70
             return calendar.date(byAdding: .day, value: 1, to: purchaseDate) ?? purchaseDate
71 71
             
72 72
         case .sevenStar:
@@ -99,30 +99,71 @@ class AddTicketViewModel: ObservableObject {
99 99
     
100 100
     // 设置表单验证
101 101
     private func setupValidation() {
102
-        Publishers.CombineLatest($amountPerBet, $numbers)
103
-            .map { amount, numbers in
104
-                // 金额验证
105
-                guard let amountValue = Double(amount), amountValue > 0 else {
106
-                    return false
107
-                }
108
-                
109
-                // 注数验证
110
-                guard self.betCount > 0 && self.betCount <= 100 else {
111
-                    return false
112
-                }
113
-                
114
-                return true
102
+        // 只监听 numbers 的变化
103
+        $numbers
104
+            .map { numbers in
105
+                // 验证 numbers 的格式和数量
106
+                return self.validateNumbers(numbers)
115 107
             }
116 108
             .assign(to: \.isValid, on: self)
117 109
             .store(in: &cancellables)
118 110
     }
119 111
     
112
+    private func validateNumbers(_ numbers: String) -> Bool {
113
+        // 1. 非空检查
114
+        guard !numbers.isEmpty else {
115
+            errorMessage = "请填写完整信息"
116
+            return false
117
+        }
118
+        
119
+        // 2. 根据彩票类型验证
120
+        switch selectedType {
121
+        case .doubleColorBall:
122
+            return validateDoubleColorBall(numbers)
123
+        case .superLotto:
124
+            return validateSuperLotto(numbers)
125
+        default:
126
+            // 其他彩票类型
127
+            return !numbers.isEmpty
128
+        }
129
+    }
130
+    
131
+    // 具体的验证方法
132
+    private func validateDoubleColorBall(_ numbers: String) -> Bool {
133
+        // 双色球格式: "01 05 12 23 28 33 + 09"
134
+        let components = numbers.split(separator: "+")
135
+        guard components.count == 2 else { return false }
136
+        
137
+        let redPart = components[0].trimmingCharacters(in: .whitespaces)
138
+        let bluePart = components[1].trimmingCharacters(in: .whitespaces)
139
+        
140
+        let redNumbers = redPart.split(separator: " ").compactMap { Int($0) }
141
+        let blueNumbers = bluePart.split(separator: " ").compactMap { Int($0) }
142
+        
143
+        // 红球6个,蓝球1个
144
+        if (redNumbers.count == 6 && blueNumbers.count == 1) {
145
+            errorMessage = ""
146
+            return true
147
+        } else {
148
+            errorMessage = "双色球个数不正确"
149
+            return false
150
+        }
151
+    }
152
+    
153
+    private func validateSuperLotto(_ numbers: String) -> Bool {
154
+        // 大乐透格式: "01 05 12 23 28 + 03 09"
155
+        // ... 类似的验证逻辑
156
+        validateDoubleColorBall(numbers)
157
+    }
158
+    
159
+    // .....
160
+    
120 161
     // 重置表单
121 162
     func resetForm() {
122 163
         selectedType = .doubleColorBall
123 164
         numbers = ""
124 165
         betCount = 1
125
-        amountPerBet = "2.00"
166
+        amountPerBet = 2
126 167
         purchaseDate = Date()
127 168
         errorMessage = ""
128 169
     }
@@ -130,19 +171,13 @@ class AddTicketViewModel: ObservableObject {
130 171
     // 保存彩票记录
131 172
     func saveTicket(context: NSManagedObjectContext) -> Bool {
132 173
         guard isValid else {
133
-            errorMessage = "请填写完整信息"
134
-            return false
135
-        }
136
-        
137
-        guard let amountValue = Double(amountPerBet), amountValue > 0 else {
138
-            errorMessage = "金额无效"
139 174
             return false
140 175
         }
141 176
         
142 177
         let ticket = LotteryTicket(context: context)
143 178
         ticket.id = UUID()
144 179
         ticket.lotteryType = selectedType
145
-        ticket.numbers = numbers.isEmpty ? "随机" : numbers
180
+        ticket.numbers = numbers
146 181
         ticket.betCount = Int16(betCount)
147 182
         ticket.amount = totalAmount
148 183
         ticket.date = purchaseDate  // 使用用户选择的购买日期
@@ -168,27 +203,12 @@ class AddTicketViewModel: ObservableObject {
168 203
     }
169 204
     
170 205
     // 快速填充测试数据
171
-    func fillSampleData() {
172
-        selectedType = LotteryType.allCases.randomElement() ?? .doubleColorBall
173
-        numbers = generateSampleNumbers(for: selectedType)
174
-        betCount = Int.random(in: 1...5)
175
-        amountPerBet = String(format: "%.2f", Double.random(in: 2.0...10.0))
176
-        purchaseDate = Date().addingTimeInterval(Double.random(in: -30...0) * 24 * 3600)
177
-    }
206
+//    func fillSampleData() {
207
+//        selectedType = LotteryType.allCases.randomElement() ?? .doubleColorBall
208
+//        numbers = generateSampleNumbers(for: selectedType)
209
+//        betCount = Int.random(in: 1...5)
210
+//        amountPerBet = String(format: "%.2f", Double.random(in: 2.0...10.0))
211
+//        purchaseDate = Date().addingTimeInterval(Double.random(in: -30...0) * 24 * 3600)
212
+//    }
178 213
     
179
-    // 生成示例号码
180
-    private func generateSampleNumbers(for type: LotteryType) -> String {
181
-        switch type {
182
-        case .doubleColorBall:
183
-            return "01 05 12 23 28 33 + 09"
184
-        case .superLotto:
185
-            return "03 07 15 20 28 + 02 09"
186
-        case .welfare3D:
187
-            return "1 2 3"
188
-        case .sevenStar:
189
-            return "1 3 5 7 9 2 4"
190
-        case .other:
191
-            return "随机号码"
192
-        }
193
-    }
194 214
 }

+ 93
- 176
LotteryTracker/Views/AddTicket/AddTicketView.swift Voir le fichier

@@ -14,12 +14,6 @@ struct AddTicketView: View {
14 14
     @Environment(\.colorScheme) private var colorScheme
15 15
     
16 16
     @StateObject private var viewModel = AddTicketViewModel()
17
-    @FocusState private var focusedField: Field?
18
-    
19
-    enum Field {
20
-        case numbers
21
-        case amount
22
-    }
23 17
     
24 18
     var body: some View {
25 19
         NavigationView {
@@ -35,23 +29,21 @@ struct AddTicketView: View {
35 29
                             .padding(.horizontal)
36 30
                         
37 31
                         // 投注号码输入
38
-                        numbersInputSection
39
-                            .padding(.horizontal)
40
-                        
41
-                        // 金额输入
42
-                        AmountInputView(
32
+                        TicketInputView(
33
+                            selectedType: $viewModel.selectedType,
34
+                            numbers: $viewModel.numbers,
43 35
                             betCount: $viewModel.betCount,
44 36
                             amountPerBet: $viewModel.amountPerBet,
45 37
                             totalAmount: viewModel.totalAmount
46 38
                         )
47 39
                         .padding(.horizontal)
48 40
                         
49
-                        // 购买日期选择(替换开奖日期)
41
+                        // 购买日期选择
50 42
                         purchaseDateSection
51 43
                             .padding(.horizontal)
52 44
                         
53 45
                         // 错误提示
54
-                        if !viewModel.errorMessage.isEmpty {
46
+                        if (!viewModel.errorMessage.isEmpty) {
55 47
                             errorMessageView
56 48
                                 .padding(.horizontal)
57 49
                         }
@@ -72,18 +64,6 @@ struct AddTicketView: View {
72 64
                         dismiss()
73 65
                     }
74 66
                 }
75
-                
76
-                ToolbarItemGroup(placement: .keyboard) {
77
-                    Spacer()
78
-                    
79
-                    Button("完成") {
80
-                        focusedField = nil
81
-                    }
82
-                }
83
-            }
84
-            .onTapGesture {
85
-                // 点击空白处隐藏键盘
86
-                focusedField = nil
87 67
             }
88 68
         }
89 69
     }
@@ -95,115 +75,84 @@ struct AddTicketView: View {
95 75
         colorScheme == .dark ? Color(.systemGray6) : Color(.systemGray6).opacity(0.3)
96 76
     }
97 77
     
98
-    // 投注号码输入
99
-    private var numbersInputSection: some View {
100
-        VStack(alignment: .leading, spacing: 12) {
101
-            HStack {
102
-                Text("投注号码")
103
-                    .font(.headline)
104
-                
105
-                Spacer()
106
-                
107
-                Button("随机生成") {
108
-                    viewModel.numbers = generateSampleNumbers(for: viewModel.selectedType)
109
-                }
110
-                .font(.caption)
111
-                .foregroundColor(.blue)
112
-            }
113
-            
114
-            VStack(alignment: .leading, spacing: 8) {
115
-                Text("请输入号码,用空格或逗号分隔")
116
-                    .font(.caption)
117
-                    .foregroundColor(.secondary)
118
-                
119
-                TextEditor(text: $viewModel.numbers)
120
-                    .font(.system(.body, design: .monospaced))
121
-                    .frame(minHeight: 80)
122
-                    .padding(8)
123
-                    .background(Color(.systemBackground))
124
-                    .cornerRadius(8)
125
-                    .overlay(
126
-                        RoundedRectangle(cornerRadius: 8)
127
-                            .stroke(focusedField == .numbers ? Color.blue : Color.gray.opacity(0.3), lineWidth: 1)
128
-                    )
129
-                    .focused($focusedField, equals: .numbers)
130
-                
131
-                Text("例如:01 05 12 23 28 33 + 09")
132
-                    .font(.caption2)
133
-                    .foregroundColor(.secondary)
134
-            }
135
-        }
136
-        .padding()
137
-        .background(Color(.systemBackground))
138
-        .cornerRadius(12)
139
-        .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
140
-    }
141
-    
142 78
     // 购买日期选择
143 79
     private var purchaseDateSection: some View {
144 80
         VStack(alignment: .leading, spacing: 12) {
145
-            HStack {
146
-                Text("购买日期")
147
-                    .font(.headline)
148
-                
149
-                Spacer()
150
-                
151
-                Button("今天") {
152
-                    viewModel.purchaseDate = Date()
81
+            Text("购买日期")
82
+                .font(.headline)
83
+            VStack {
84
+                HStack {
85
+                    Label {
86
+                        Text("点按日期进行选择")
87
+                    } icon: {
88
+                        Image(systemName: "hand.tap")
89
+                    }
90
+                    .font(.caption)
91
+                    .foregroundColor(.blue.opacity(0.7))
92
+                    
93
+                    Spacer()
94
+                    
95
+                    Button("今天") {
96
+                        viewModel.purchaseDate = Date()
97
+                    }
98
+                    .font(.caption)
99
+                    .foregroundColor(.blue)
100
+                    
101
+                    Button("昨天") {
102
+                        viewModel.purchaseDate = Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date()
103
+                    }
104
+                    .font(.caption)
105
+                    .foregroundColor(.blue)
153 106
                 }
154
-                .font(.caption)
155
-                .foregroundColor(.blue)
156 107
                 
157
-                Button("昨天") {
158
-                    viewModel.purchaseDate = Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date()
108
+                DatePicker(
109
+                    "选择购买日期",
110
+                    selection: $viewModel.purchaseDate,
111
+                    in: Date().addingTimeInterval(-365 * 24 * 3600)...Date(), // 过去一年到现在
112
+                    displayedComponents: .date
113
+                )
114
+                .datePickerStyle(.graphical)
115
+                .padding()
116
+                .cornerRadius(12)
117
+                .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
118
+                .background(
119
+                    // 日历专用背景色
120
+                    Color(red: 0.98, green: 0.97, blue: 0.95)
121
+                )
122
+                
123
+                // 开奖信息显示(只读)
124
+                HStack {
125
+                    Image(systemName: "calendar.badge.clock")
126
+                        .foregroundColor(.blue)
127
+                    
128
+                    VStack(alignment: .leading, spacing: 4) {
129
+                        Text("购买时间: \(viewModel.formattedPurchaseDate)")
130
+                            .font(.subheadline)
131
+                            .foregroundColor(.secondary)
132
+                        
133
+                        Text("预计开奖: \(viewModel.formattedDrawDate)")
134
+                            .font(.subheadline)
135
+                            .foregroundColor(.green)
136
+                    }
137
+                    
138
+                    Spacer()
139
+                    
140
+                    
141
+                    Text("\(daysUntilDraw)")
142
+                        .font(.caption)
143
+                        .padding(.horizontal, 8)
144
+                        .padding(.vertical, 4)
145
+                        .background(Color.blue.opacity(0.1))
146
+                        .foregroundColor(.blue)
147
+                        .cornerRadius(6)
159 148
                 }
160
-                .font(.caption)
161
-                .foregroundColor(.blue)
149
+                .padding(.horizontal, 4)
162 150
             }
163
-            
164
-            DatePicker(
165
-                "选择购买日期",
166
-                selection: $viewModel.purchaseDate,
167
-                in: Date().addingTimeInterval(-365 * 24 * 3600)...Date(), // 过去一年到现在
168
-                displayedComponents: .date
169
-            )
170
-            .datePickerStyle(.graphical)
171 151
             .padding()
172 152
             .background(Color(.systemBackground))
173 153
             .cornerRadius(12)
174 154
             .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
175
-            
176
-            // 开奖信息显示(只读)
177
-            HStack {
178
-                Image(systemName: "calendar.badge.clock")
179
-                    .foregroundColor(.blue)
180
-                
181
-                VStack(alignment: .leading, spacing: 4) {
182
-                    Text("购买时间: \(viewModel.formattedPurchaseDate)")
183
-                        .font(.subheadline)
184
-                        .foregroundColor(.secondary)
185
-                    
186
-                    Text("预计开奖: \(viewModel.formattedDrawDate)")
187
-                        .font(.subheadline)
188
-                        .foregroundColor(.green)
189
-                }
190
-                
191
-                Spacer()
192
-                
193
-                Text("\(daysUntilDraw)天后")
194
-                    .font(.caption)
195
-                    .padding(.horizontal, 8)
196
-                    .padding(.vertical, 4)
197
-                    .background(Color.blue.opacity(0.1))
198
-                    .foregroundColor(.blue)
199
-                    .cornerRadius(6)
200
-            }
201
-            .padding(.horizontal, 4)
202 155
         }
203
-        .padding()
204
-        .background(Color(.systemBackground))
205
-        .cornerRadius(12)
206
-        .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
207 156
     }
208 157
     
209 158
     // 错误提示
@@ -246,24 +195,24 @@ struct AddTicketView: View {
246 195
             .disabled(!viewModel.isValid)
247 196
             
248 197
             // 测试数据按钮
249
-            Button(action: {
250
-                viewModel.fillSampleData()
251
-            }) {
252
-                HStack {
253
-                    Spacer()
254
-                    
255
-                    Image(systemName: "wand.and.stars")
256
-                    
257
-                    Text("填充测试数据")
258
-                        .font(.subheadline)
259
-                    
260
-                    Spacer()
261
-                }
262
-                .padding()
263
-                .background(Color.green.opacity(0.1))
264
-                .foregroundColor(.green)
265
-                .cornerRadius(12)
266
-            }
198
+//            Button(action: {
199
+//                viewModel.fillSampleData()
200
+//            }) {
201
+//                HStack {
202
+//                    Spacer()
203
+//                    
204
+//                    Image(systemName: "wand.and.stars")
205
+//                    
206
+//                    Text("填充测试数据")
207
+//                        .font(.subheadline)
208
+//                    
209
+//                    Spacer()
210
+//                }
211
+//                .padding()
212
+//                .background(Color.green.opacity(0.1))
213
+//                .foregroundColor(.green)
214
+//                .cornerRadius(12)
215
+//            }
267 216
             
268 217
             // 重置按钮
269 218
             Button(action: {
@@ -274,7 +223,7 @@ struct AddTicketView: View {
274 223
                     
275 224
                     Image(systemName: "arrow.counterclockwise")
276 225
                     
277
-                    Text("重置表单")
226
+                    Text("重置")
278 227
                         .font(.subheadline)
279 228
                     
280 229
                     Spacer()
@@ -290,10 +239,11 @@ struct AddTicketView: View {
290 239
     // MARK: - 计算属性
291 240
     
292 241
     // 距离开奖天数
293
-    private var daysUntilDraw: Int {
242
+    private var daysUntilDraw: String {
294 243
         let calendar = Calendar.current
295 244
         let components = calendar.dateComponents([.day], from: viewModel.purchaseDate, to: viewModel.drawDate)
296
-        return max(0, components.day ?? 0)
245
+        let leftDays = max(0, components.day ?? 0)
246
+        return leftDays == 0 ? "今天" : String(leftDays) + "天后"
297 247
     }
298 248
     
299 249
     // MARK: - 方法
@@ -311,38 +261,5 @@ struct AddTicketView: View {
311 261
             print("❌ 保存失败")
312 262
         }
313 263
     }
314
-    
315
-    // 生成示例号码
316
-    private func generateSampleNumbers(for type: LotteryType) -> String {
317
-        switch type {
318
-        case .doubleColorBall:
319
-            let redBalls = (1...33).shuffled().prefix(6).map { String(format: "%02d", $0) }
320
-            let blueBall = String(format: "%02d", Int.random(in: 1...16))
321
-            return "\(redBalls.joined(separator: " ")) + \(blueBall)"
322
-            
323
-        case .superLotto:
324
-            let frontBalls = (1...35).shuffled().prefix(5).map { String(format: "%02d", $0) }
325
-            let backBalls = (1...12).shuffled().prefix(2).map { String(format: "%02d", $0) }
326
-            return "\(frontBalls.joined(separator: " ")) + \(backBalls.joined(separator: " "))"
327
-            
328
-        case .welfare3D:
329
-            let numbers = (0...9).shuffled().prefix(3).map { String($0) }
330
-            return numbers.joined(separator: " ")
331
-            
332
-        case .sevenStar:
333
-            let numbers = (0...9).shuffled().prefix(7).map { String($0) }
334
-            return numbers.joined(separator: " ")
335
-            
336
-        case .other:
337
-            return "随机号码"
338
-        }
339
-    }
340 264
 }
341 265
 
342
-// 预览
343
-struct AddTicketView_Previews: PreviewProvider {
344
-    static var previews: some View {
345
-        AddTicketView()
346
-            .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
347
-    }
348
-}

+ 0
- 212
LotteryTracker/Views/AddTicket/AmountInputView.swift Voir le fichier

@@ -1,212 +0,0 @@
1
-//
2
-//  AmountInputView.swift
3
-//  LotteryTracker
4
-//
5
-//  Created by aaa on 2026/1/22.
6
-//
7
-
8
-import SwiftUI
9
-
10
-struct AmountInputView: View {
11
-    @Binding var betCount: Int
12
-    @Binding var amountPerBet: String
13
-    @FocusState private var isAmountFocused: Bool
14
-    
15
-    let totalAmount: Float
16
-    
17
-    // 常用注数选项
18
-    private let betCountOptions = [1, 3, 5, 10, 20]
19
-    
20
-    // 常用金额选项
21
-    private let amountOptions = [2.0, 5.0, 10.0, 20.0, 50.0]
22
-    
23
-    var body: some View {
24
-        VStack(alignment: .leading, spacing: 20) {
25
-            // 注数选择
26
-            VStack(alignment: .leading, spacing: 12) {
27
-                Text("注数")
28
-                    .font(.headline)
29
-                
30
-                // 注数选择器
31
-                HStack {
32
-                    Stepper(value: $betCount, in: 1...100) {
33
-                        HStack {
34
-                            Text("\(betCount) 注")
35
-                                .font(.body)
36
-                                .foregroundColor(.primary)
37
-                            
38
-                            Spacer()
39
-                            
40
-                            Text(Formatters.formatCurrency(totalAmount))
41
-                                .font(.body.bold())
42
-                                .foregroundColor(.blue)
43
-                        }
44
-                    }
45
-                }
46
-                
47
-                // 快速选择按钮
48
-                ScrollView(.horizontal, showsIndicators: false) {
49
-                    HStack(spacing: 10) {
50
-                        ForEach(betCountOptions, id: \.self) { count in
51
-                            Button(action: {
52
-                                withAnimation {
53
-                                    betCount = count
54
-                                }
55
-                            }) {
56
-                                Text("\(count)注")
57
-                                    .font(.system(size: 14))
58
-                                    .padding(.horizontal, 12)
59
-                                    .padding(.vertical, 6)
60
-                                    .background(betCount == count ? Color.blue : Color.gray.opacity(0.1))
61
-                                    .foregroundColor(betCount == count ? .white : .primary)
62
-                                    .cornerRadius(8)
63
-                            }
64
-                        }
65
-                    }
66
-                    .padding(.horizontal, 2)
67
-                }
68
-            }
69
-            
70
-            Divider()
71
-            
72
-            // 每注金额
73
-            VStack(alignment: .leading, spacing: 12) {
74
-                Text("每注金额")
75
-                    .font(.headline)
76
-                
77
-                // 金额输入
78
-                HStack {
79
-                    Text("¥")
80
-                        .font(.title3)
81
-                        .foregroundColor(.secondary)
82
-                    
83
-                    TextField("0.00", text: $amountPerBet)
84
-                        .font(.title3)
85
-                        .keyboardType(.decimalPad)
86
-                        .focused($isAmountFocused)
87
-                        .padding(.vertical, 8)
88
-                        .padding(.horizontal, 12)
89
-                        .background(Color(.systemGray6))
90
-                        .cornerRadius(8)
91
-                        .overlay(
92
-                            RoundedRectangle(cornerRadius: 8)
93
-                                .stroke(isAmountFocused ? Color.blue : Color.clear, lineWidth: 1)
94
-                        )
95
-                        .onChange(of: amountPerBet) { oldValue, newValue in
96
-                            // iOS 17 新API:接收 oldValue 和 newValue 两个参数
97
-                            
98
-                            // 限制输入为数字和小数点
99
-                            let filtered = newValue.filter { "0123456789.".contains($0) }
100
-                            if filtered != newValue {
101
-                                amountPerBet = filtered
102
-                            }
103
-                            
104
-                            // 限制小数点后最多两位
105
-                            if let dotIndex = filtered.firstIndex(of: ".") {
106
-                                let decimalPart = filtered[filtered.index(after: dotIndex)...]
107
-                                if decimalPart.count > 2 {
108
-                                    amountPerBet = String(filtered.prefix(upTo: filtered.index(dotIndex, offsetBy: 3)))
109
-                                }
110
-                            }
111
-                        }
112
-                }
113
-                
114
-                // 快速金额按钮
115
-                ScrollView(.horizontal, showsIndicators: false) {
116
-                    HStack(spacing: 10) {
117
-                        ForEach(amountOptions, id: \.self) { amount in
118
-                            Button(action: {
119
-                                withAnimation {
120
-                                    amountPerBet = String(format: "%.2f", amount)
121
-                                }
122
-                            }) {
123
-                                Text(Formatters.formatCurrency(Float(amount)))
124
-                                    .font(.system(size: 14))
125
-                                    .padding(.horizontal, 12)
126
-                                    .padding(.vertical, 6)
127
-                                    .background(amountPerBet == String(format: "%.2f", amount) ? Color.green : Color.gray.opacity(0.1))
128
-                                    .foregroundColor(amountPerBet == String(format: "%.2f", amount) ? .white : .primary)
129
-                                    .cornerRadius(8)
130
-                            }
131
-                        }
132
-                    }
133
-                    .padding(.horizontal, 2)
134
-                }
135
-            }
136
-            
137
-            Divider()
138
-            
139
-            // 总计显示
140
-            VStack(alignment: .leading, spacing: 8) {
141
-                Text("总计")
142
-                    .font(.headline)
143
-                
144
-                HStack {
145
-                    VStack(alignment: .leading, spacing: 4) {
146
-                        Text("\(betCount) 注 × ¥\(Double(amountPerBet) ?? 0, specifier: "%.2f")/注")
147
-                            .font(.subheadline)
148
-                            .foregroundColor(.secondary)
149
-                        
150
-                        Text(Formatters.formatCurrency(totalAmount))
151
-                            .font(.system(size: 32, weight: .bold))
152
-                            .foregroundColor(.blue)
153
-                    }
154
-                    
155
-                    Spacer()
156
-                    
157
-                    Image(systemName: "creditcard.fill")
158
-                        .font(.system(size: 36))
159
-                        .foregroundColor(.green.opacity(0.7))
160
-                }
161
-                .padding()
162
-                .background(
163
-                    RoundedRectangle(cornerRadius: 12)
164
-                        .fill(Color.blue.opacity(0.05))
165
-                        .overlay(
166
-                            RoundedRectangle(cornerRadius: 12)
167
-                                .stroke(Color.blue.opacity(0.2), lineWidth: 1)
168
-                        )
169
-                )
170
-            }
171
-        }
172
-        .padding(.vertical, 8)
173
-    }
174
-}
175
-
176
-// 预览
177
-struct AmountInputView_Previews: PreviewProvider {
178
-    static var previews: some View {
179
-        // 方法1:使用简单的 State 包装
180
-        Group {
181
-            // 预览1:默认状态
182
-            AmountInputViewPreviewWrapper()
183
-            
184
-            // 预览2:大额状态
185
-            AmountInputViewPreviewWrapper(betCount: 10, amountPerBet: "50.00")
186
-            
187
-            // 预览3:小数金额
188
-            AmountInputViewPreviewWrapper(betCount: 3, amountPerBet: "2.50")
189
-        }
190
-    }
191
-}
192
-
193
-// 预览包装器
194
-struct AmountInputViewPreviewWrapper: View {
195
-    @State private var betCount: Int
196
-    @State private var amountPerBet: String
197
-    
198
-    init(betCount: Int = 3, amountPerBet: String = "2.00") {
199
-        self._betCount = State(initialValue: betCount)
200
-        self._amountPerBet = State(initialValue: amountPerBet)
201
-    }
202
-    
203
-    var body: some View {
204
-        AmountInputView(
205
-            betCount: $betCount,
206
-            amountPerBet: $amountPerBet,
207
-            totalAmount: (Float(amountPerBet) ?? 0) * Float(betCount)
208
-        )
209
-        .padding()
210
-        .previewLayout(.sizeThatFits)
211
-    }
212
-}

+ 416
- 0
LotteryTracker/Views/AddTicket/DoubleColorBallCompoents.swift Voir le fichier

@@ -0,0 +1,416 @@
1
+//
2
+//  DoubleColorBallCompoents.swift
3
+//  LotteryTracker
4
+//
5
+//  Created by aaa on 2026/1/25.
6
+//
7
+
8
+import SwiftUI
9
+
10
+struct DoubleColorBallSection: View {
11
+    @Binding var numbers: String
12
+    
13
+    @State private var redNumbers: [Int] = []
14
+    @State private var blueNumbers: [Int] = []
15
+    @State private var betCount = 1
16
+    @State private var selectedType = LotteryType.doubleColorBall
17
+    
18
+    
19
+    var body: some View {
20
+        VStack(alignment: .leading, spacing: 16) {
21
+            // 头部标题
22
+            SectionHeader(title: "选择号码", icon: "number.circle")
23
+            
24
+            // 红球选择区
25
+            VStack(alignment: .leading, spacing: 12) {
26
+                NumberSelectionGrid(
27
+                    title: "红球区",
28
+                    color: .red,
29
+                    range: 1...33,
30
+                    maxSelection: 6,
31
+                    selectedNumbers: $redNumbers
32
+                )
33
+            }
34
+            
35
+            // 蓝球选择区
36
+            VStack(alignment: .leading, spacing: 12) {
37
+                NumberSelectionGrid(
38
+                    title: "蓝球区",
39
+                    color: .blue,
40
+                    range: 1...16,
41
+                    maxSelection: 1,
42
+                    selectedNumbers: $blueNumbers
43
+                )
44
+            }
45
+            
46
+            // 快速操作栏
47
+            QuickActionsBar(
48
+                onRandom: generateRandomNumbers,
49
+                onClear: clearAllNumbers
50
+            )
51
+            
52
+            // 红球/蓝球已选显示
53
+            if (!redNumbers.isEmpty || !blueNumbers.isEmpty) {
54
+                SelectedNumbersView(
55
+                    reds: redNumbers,
56
+                    blues: blueNumbers,
57
+                    maxCount: 7,
58
+                    onClear: {
59
+                        redNumbers.removeAll()
60
+                        blueNumbers.removeAll()
61
+                    }
62
+                )
63
+                
64
+            }
65
+        }
66
+        .onChange(of: redNumbers) {
67
+            updateNumbers()
68
+        }
69
+        .onChange(of: blueNumbers) {
70
+            updateNumbers()
71
+        }
72
+    }
73
+    
74
+    // MARK: -方法
75
+    private func generateRandomNumbers() {
76
+        redNumbers = Array(Set((1...33).shuffled().prefix(6))).sorted()
77
+        blueNumbers = [Int.random(in: 1...16)]
78
+    }
79
+    
80
+    private func clearAllNumbers() {
81
+        withAnimation {
82
+            redNumbers.removeAll()
83
+            blueNumbers.removeAll()
84
+        }
85
+    }
86
+    
87
+    private func updateNumbers() {
88
+        let redStr = redNumbers.sorted().map { String(format: "%02d", $0) }.joined(separator: " ")
89
+        let blueStr = blueNumbers.sorted().map { String(format: "%02d", $0) }.joined(separator: " ")
90
+        numbers = "\(redStr) + \(blueStr)"
91
+    }
92
+}
93
+
94
+
95
+// MARK: - 头部组件
96
+struct SectionHeader: View {
97
+    let title: String
98
+    let icon: String
99
+    
100
+    var body: some View {
101
+        HStack {
102
+            Label(title, systemImage: icon)
103
+                .font(.headline)
104
+                .foregroundColor(.primary)
105
+            
106
+            Spacer()
107
+            
108
+            // 状态指示器
109
+            Capsule()
110
+                .fill(Color.green.opacity(0.1))
111
+                .frame(width: 80, height: 24)
112
+                .overlay(
113
+                    HStack(spacing: 4) {
114
+                        Circle()
115
+                            .fill(Color.green)
116
+                            .frame(width: 8, height: 8)
117
+                        
118
+                        Text("可选")
119
+                            .font(.caption2)
120
+                            .foregroundColor(.green)
121
+                    }
122
+                )
123
+        }
124
+    }
125
+}
126
+
127
+
128
+// MARK: - 数字选择网格
129
+struct NumberSelectionGrid: View {
130
+    let title: String
131
+    let color: Color
132
+    let range: ClosedRange<Int>
133
+    let maxSelection: Int
134
+    @Binding var selectedNumbers: [Int]
135
+    
136
+    private let columns = Array(repeating: GridItem(.flexible()), count: 10)
137
+    
138
+    var body: some View {
139
+        VStack(alignment: .leading, spacing: 8) {
140
+            // 标题和计数
141
+            HStack {
142
+                Text(title)
143
+                    .font(.subheadline)
144
+                    .fontWeight(.semibold)
145
+                    .foregroundColor(color)
146
+                
147
+                Spacer()
148
+                
149
+                Text("\(selectedNumbers.count)/\(maxSelection)")
150
+                    .font(.caption)
151
+                    .fontWeight(.medium)
152
+                    .foregroundColor(.secondary)
153
+                    .padding(.horizontal, 8)
154
+                    .padding(.vertical, 2)
155
+                    .background(
156
+                        Capsule()
157
+                            .fill(color.opacity(0.1))
158
+                    )
159
+            }
160
+            .padding(.horizontal, 4)
161
+            
162
+            // 数字网格
163
+            LazyVGrid(columns: columns, spacing: 10) {
164
+                ForEach(range, id: \.self) { number in
165
+                    NumberButton(
166
+                        number: number,
167
+                        color: color,
168
+                        isSelected: selectedNumbers.contains(number),
169
+                        isSelectable: selectedNumbers.count < maxSelection || selectedNumbers.contains(number)
170
+                    ) {
171
+                        toggleNumber(number)
172
+                    }
173
+                }
174
+            }
175
+        }
176
+        .padding(16)
177
+        .background(Color(.secondarySystemBackground))
178
+        .cornerRadius(12)
179
+        .overlay(
180
+            RoundedRectangle(cornerRadius: 12)
181
+                .stroke(color.opacity(0.2), lineWidth: 1)
182
+        )
183
+    }
184
+    
185
+    private func toggleNumber(_ number: Int) {
186
+        withAnimation(.spring(response: 0.3)) {
187
+            if selectedNumbers.contains(number) {
188
+                selectedNumbers.removeAll { $0 == number }
189
+            } else if selectedNumbers.count < maxSelection {
190
+                selectedNumbers.append(number)
191
+                // 触觉反馈
192
+                let generator = UIImpactFeedbackGenerator(style: .light)
193
+                generator.impactOccurred()
194
+            }
195
+        }
196
+    }
197
+}
198
+
199
+// MARK: - 数字按钮
200
+struct NumberButton: View {
201
+    let number: Int
202
+    let color: Color
203
+    let isSelected: Bool
204
+    let isSelectable: Bool
205
+    let action: () -> Void
206
+    
207
+    @State private var isPressed = false
208
+    
209
+    var body: some View {
210
+        Button(action: handleAction) {
211
+            ZStack {
212
+                // 背景圆圈
213
+                Circle()
214
+                    .fill(backgroundColor)
215
+                    .frame(width: 32, height: 32)
216
+                
217
+                // 数字
218
+                Text(String(format: "%02d", number))
219
+                    .font(.system(size: 16, weight: .semibold, design: .rounded))
220
+                    .foregroundColor(textColor)
221
+                
222
+                // 选中标记
223
+                if isSelected {
224
+                    Circle()
225
+                        .stroke(color, lineWidth: 2)
226
+                        .frame(width: 32, height: 32)
227
+                    
228
+                    Image(systemName: "checkmark.circle.fill")
229
+                        .font(.system(size: 12))
230
+                        .foregroundColor(color)
231
+                        .background(Color.white)
232
+                        .clipShape(Circle())
233
+                        .offset(x: 16, y: -16)
234
+                }
235
+            }
236
+        }
237
+        .buttonStyle(PressableButtonStyle())
238
+        .disabled(!isSelectable && !isSelected)
239
+        .opacity(isSelectable || isSelected ? 1.0 : 0.4)
240
+    }
241
+    
242
+    private func handleAction() {
243
+        if isSelectable {
244
+            action()
245
+        } else {
246
+            let generator = UINotificationFeedbackGenerator()
247
+            generator.notificationOccurred(.warning)
248
+        }
249
+    }
250
+    
251
+    private var backgroundColor: Color {
252
+        isSelected ? color.opacity(0.15) : Color(.systemBackground)
253
+    }
254
+    
255
+    private var textColor: Color {
256
+        isSelected ? color : .primary
257
+    }
258
+}
259
+
260
+struct PressableButtonStyle: ButtonStyle {
261
+    func makeBody(configuration: Configuration) -> some View {
262
+        configuration.label
263
+            .scaleEffect(configuration.isPressed ? 0.95 : 1.0)
264
+            .opacity(configuration.isPressed ? 0.9 : 1.0)
265
+            .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
266
+    }
267
+}
268
+
269
+// MARK: - 已选号码显示
270
+struct SelectedNumbersView: View {
271
+    let reds: [Int]
272
+    let blues: [Int]
273
+    let maxCount: Int
274
+    let onClear: () -> Void
275
+    
276
+    var body: some View {
277
+        HStack(spacing: 8) {
278
+            // 标签
279
+            Capsule()
280
+                .fill(Color(.gray).opacity(0.1))
281
+                .frame(width: 36, height: 28)
282
+                .overlay(
283
+                    Text("已选")
284
+                        .font(.caption2)
285
+                        .fontWeight(.medium)
286
+                        .foregroundColor(Color(.black))
287
+                )
288
+            
289
+            // 号码显示
290
+            ScrollView(.horizontal, showsIndicators: false) {
291
+                HStack(spacing: 6) {
292
+                    ForEach(reds.sorted(), id: \.self) { number in
293
+                        Text(String(format: "%02d", number))
294
+                            .font(.system(size: 14, weight: .medium, design: .monospaced))
295
+                            .frame(width: 28, height: 28)
296
+                            .background(Color(.red).opacity(0.15))
297
+                            .foregroundColor(Color(.red))
298
+                            .clipShape(Circle())
299
+                    }
300
+                    
301
+                    ForEach(blues.sorted(), id: \.self) { number in
302
+                        Text(String(format: "%02d", number))
303
+                            .font(.system(size: 14, weight: .medium, design: .monospaced))
304
+                            .frame(width: 28, height: 28)
305
+                            .background(Color(.blue).opacity(0.15))
306
+                            .foregroundColor(Color(.blue))
307
+                            .clipShape(Circle())
308
+                    }
309
+                }
310
+            }
311
+            
312
+            Spacer()
313
+            
314
+            // 清空按钮
315
+            if (!reds.isEmpty || !blues.isEmpty) {
316
+                Button(action: {
317
+                    withAnimation {
318
+                        onClear()
319
+                    }
320
+                }) {
321
+                    Image(systemName: "xmark.circle.fill")
322
+                        .font(.system(size: 16))
323
+                        .foregroundColor(.secondary)
324
+                        .padding(4)
325
+                }
326
+            }
327
+        }
328
+        .padding(.horizontal, 16)
329
+        .padding(.vertical, 12)
330
+        .background(Color(.systemBackground))
331
+        .cornerRadius(10)
332
+        .shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1)
333
+    }
334
+}
335
+
336
+// MARK: - 快速操作栏
337
+struct QuickActionsBar: View {
338
+    let onRandom: () -> Void
339
+    let onClear: () -> Void
340
+    
341
+    var body: some View {
342
+        HStack(spacing: 12) {
343
+            // 随机生成
344
+            ActionButton(
345
+                title: "随机一注",
346
+                icon: "dice.fill",
347
+                color: .purple,
348
+                action: onRandom
349
+            )
350
+            
351
+            // 清空全部
352
+            ActionButton(
353
+                title: "清空重选",
354
+                icon: "trash",
355
+                color: .gray,
356
+                action: onClear
357
+            )
358
+        }
359
+    }
360
+}
361
+
362
+struct ActionButton: View {
363
+    let title: String
364
+    let icon: String
365
+    let color: Color
366
+    let action: () -> Void
367
+    
368
+    var body: some View {
369
+        Button(action: {
370
+            action()
371
+            let generator = UIImpactFeedbackGenerator(style: .light)
372
+            generator.impactOccurred()
373
+        }) {
374
+            HStack(spacing: 6) {
375
+                Image(systemName: icon)
376
+                    .font(.system(size: 14))
377
+                
378
+                Text(title)
379
+                    .font(.caption)
380
+                    .fontWeight(.medium)
381
+            }
382
+            .padding(.horizontal, 16)
383
+            .padding(.vertical, 10)
384
+            .frame(maxWidth: .infinity)
385
+            .background(color.opacity(0.1))
386
+            .foregroundColor(color)
387
+            .cornerRadius(10)
388
+        }
389
+    }
390
+}
391
+
392
+
393
+struct ChipButton: View {
394
+    let title: String
395
+    let isSelected: Bool
396
+    let action: () -> Void
397
+    
398
+    var body: some View {
399
+        Button(action: action) {
400
+            Text(title)
401
+                .font(.system(size: 14, weight: .medium))
402
+                .padding(.horizontal, 16)
403
+                .padding(.vertical, 8)
404
+                .background(
405
+                    Capsule()
406
+                        .fill(isSelected ? Color.blue : Color.gray.opacity(0.1))
407
+                )
408
+                .foregroundColor(isSelected ? .white : .primary)
409
+                .overlay(
410
+                    Capsule()
411
+                        .stroke(isSelected ? Color.blue : Color.gray.opacity(0.3), lineWidth: 1)
412
+                )
413
+        }
414
+    }
415
+}
416
+

+ 12
- 38
LotteryTracker/Views/AddTicket/LotteryTypePicker.swift Voir le fichier

@@ -12,10 +12,10 @@ struct LotteryTypePicker: View {
12 12
     
13 13
     // 彩票类型描述
14 14
     private let typeDescriptions: [LotteryType: String] = [
15
-        .doubleColorBall: "红球6 + 篮球1",
16
-        .superLotto: "前区5 + 后区2",
17
-        .welfare3D: "3位数字,000-999",
18
-        .sevenStar: "7位数字,每位0-9",
15
+        .doubleColorBall: "红球6 + 篮球1",
16
+        .superLotto: "前区5 + 后区2",
17
+        .happy8: "3位数 每位0-9",
18
+        .sevenStar: "7位数 每位0-9",
19 19
         .other: "其他彩票类型"
20 20
     ]
21 21
     
@@ -40,6 +40,7 @@ struct LotteryTypePicker: View {
40 40
                         }
41 41
                     }
42 42
                 }
43
+                .padding(.vertical, 2)
43 44
                 .padding(.horizontal, 4)
44 45
             }
45 46
         }
@@ -59,10 +60,12 @@ struct LotteryTypeCard: View {
59 60
                 Circle()
60 61
                     .fill(backgroundColor)
61 62
                     .frame(width: 60, height: 60)
63
+                    .scaleEffect(isSelected ? 1.15 : 1.0)
62 64
                 
63 65
                 Image(systemName: iconName)
64 66
                     .font(.system(size: 24))
65 67
                     .foregroundColor(iconColor)
68
+                    .scaleEffect(isSelected ? 1.2 : 1.0)
66 69
             }
67 70
             
68 71
             // 类型名称
@@ -80,8 +83,9 @@ struct LotteryTypeCard: View {
80 83
                 .lineLimit(2)
81 84
                 .frame(width: 80)
82 85
         }
83
-        .padding(.vertical, 12)
84
-        .padding(.horizontal, 8)
86
+        // 放大5%,需要额外padding来补偿
87
+        .padding(.vertical, isSelected ? 10 : 12)
88
+        .padding(.horizontal, isSelected ? 6 : 8)
85 89
         .background(
86 90
             RoundedRectangle(cornerRadius: 12)
87 91
                 .fill(isSelected ? Color.blue.opacity(0.05) : Color(.systemBackground))
@@ -97,7 +101,7 @@ struct LotteryTypeCard: View {
97 101
         switch type {
98 102
         case .doubleColorBall: return "circle.grid.2x2.fill"
99 103
         case .superLotto: return "8.circle.fill"
100
-        case .welfare3D: return "3.circle.fill"
104
+        case .happy8: return "3.circle.fill"
101 105
         case .sevenStar: return "star.fill"
102 106
         case .other: return "ticket.fill"
103 107
         }
@@ -107,7 +111,7 @@ struct LotteryTypeCard: View {
107 111
         switch type {
108 112
         case .doubleColorBall: return .red
109 113
         case .superLotto: return .blue
110
-        case .welfare3D: return .green
114
+        case .happy8: return .green
111 115
         case .sevenStar: return .purple
112 116
         case .other: return .gray
113 117
         }
@@ -117,33 +121,3 @@ struct LotteryTypeCard: View {
117 121
         isSelected ? iconColor.opacity(0.1) : Color(.systemGray6)
118 122
     }
119 123
 }
120
-
121
-// 预览
122
-struct LotteryTypePicker_Previews: PreviewProvider {
123
-    static var previews: some View {
124
-        StatefulPreviewWrapper(LotteryType.doubleColorBall) { binding in
125
-            VStack {
126
-                LotteryTypePicker(selectedType: binding)
127
-                    .padding()
128
-                
129
-                Text("当前选择: \(binding.wrappedValue.rawValue)")
130
-                    .padding()
131
-            }
132
-        }
133
-    }
134
-}
135
-
136
-// 用于预览的状态包装器
137
-struct StatefulPreviewWrapper<T, Content: View>: View where Content: View {
138
-    @State var value: T
139
-    let content: (Binding<T>) -> Content
140
-    
141
-    var body: some View {
142
-        content($value)
143
-    }
144
-    
145
-    init(_ value: T, @ViewBuilder content: @escaping (Binding<T>) -> Content) {
146
-        self._value = State(initialValue: value)
147
-        self.content = content
148
-    }
149
-}

+ 251
- 0
LotteryTracker/Views/AddTicket/TicketInputView.swift Voir le fichier

@@ -0,0 +1,251 @@
1
+//
2
+//  AmountInputView.swift
3
+//  LotteryTracker
4
+//
5
+//  Created by aaa on 2026/1/22.
6
+//
7
+
8
+import SwiftUI
9
+
10
+struct TicketInputView: View {
11
+    @Binding var selectedType: LotteryType
12
+    @Binding var numbers: String
13
+    @Binding var betCount: Int
14
+    @Binding var amountPerBet: Int
15
+    
16
+    @FocusState private var focusedField: Field?
17
+    
18
+    enum Field {
19
+        case numbers
20
+        case amount
21
+    }
22
+    
23
+    let totalAmount: Float
24
+    
25
+    // 常用倍数选项
26
+    private let betCountOptions = [1, 5, 10, 20]
27
+    
28
+    var body: some View {
29
+        VStack(alignment: .leading, spacing: 20) {
30
+            // 投注选择
31
+            if (selectedType == .doubleColorBall) {
32
+                DoubleColorBallSection(numbers: $numbers)
33
+            } else if (selectedType == .superLotto) {
34
+                numbersInputSection
35
+            } else if (selectedType == .happy8) {
36
+                numbersInputSection
37
+            } else if (selectedType == .sevenStar) {
38
+                numbersInputSection
39
+            } else {
40
+                
41
+            }
42
+            
43
+            // 注数选择
44
+            betCountSection
45
+            
46
+            Divider()
47
+            
48
+//            // 总计显示 TODO 添加多注时用到
49
+//            totalAmountSection
50
+            
51
+        }
52
+        .padding(.vertical, 8)
53
+    }
54
+    
55
+    
56
+    // MARK: - 组件
57
+    
58
+    private var numbersInputSection: some View {
59
+        VStack(alignment: .leading, spacing: 12) {
60
+            Text("投注号码")
61
+                .font(.headline)
62
+            VStack {
63
+                HStack {
64
+                    Spacer()
65
+                    
66
+                    Button("随机生成") {
67
+                        numbers = generateSampleNumbers(for: selectedType)
68
+                    }
69
+                    .font(.caption)
70
+                    .foregroundColor(.blue)
71
+                }
72
+                
73
+                VStack(alignment: .leading, spacing: 8) {
74
+                    Text("请输入号码,用空格或逗号分隔")
75
+                        .font(.caption)
76
+                        .foregroundColor(.secondary)
77
+                    
78
+                    TextEditor(text: $numbers)
79
+                        .font(.system(.body, design: .monospaced))
80
+                        .frame(minHeight: 80)
81
+                        .padding(8)
82
+                        .background(Color(.systemBackground))
83
+                        .cornerRadius(8)
84
+                        .overlay(
85
+                            RoundedRectangle(cornerRadius: 8)
86
+                                .stroke(focusedField == .numbers ? Color.blue : Color.gray.opacity(0.3), lineWidth: 1)
87
+                        )
88
+                        .focused($focusedField, equals: .numbers)
89
+                    
90
+                    Text("例如:01 05 12 23 28 33 + 09")
91
+                        .font(.caption2)
92
+                        .foregroundColor(.secondary)
93
+                }
94
+            }
95
+            .padding()
96
+            .background(Color(.systemBackground))
97
+            .cornerRadius(12)
98
+            .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
99
+        }
100
+    }
101
+    
102
+    private var totalAmountSection: some View {
103
+        VStack(alignment: .leading, spacing: 8) {
104
+            Text("总计")
105
+                .font(.headline)
106
+            
107
+            HStack {
108
+                VStack(alignment: .leading, spacing: 4) {
109
+                    Text("\(betCount) 倍 × ¥\(Double(amountPerBet), specifier: "%.2f")/注")
110
+                        .font(.subheadline)
111
+                        .foregroundColor(.secondary)
112
+                    
113
+                    Text(Formatters.formatCurrency(totalAmount))
114
+                        .font(.system(size: 32, weight: .bold))
115
+                        .foregroundColor(.blue)
116
+                }
117
+                
118
+                Spacer()
119
+                
120
+                Image(systemName: "creditcard.fill")
121
+                    .font(.system(size: 36))
122
+                    .foregroundColor(.green.opacity(0.7))
123
+            }
124
+            .padding()
125
+            .background(
126
+                RoundedRectangle(cornerRadius: 12)
127
+                    .fill(Color.blue.opacity(0.05))
128
+                    .overlay(
129
+                        RoundedRectangle(cornerRadius: 12)
130
+                            .stroke(Color.blue.opacity(0.2), lineWidth: 1)
131
+                    )
132
+            )
133
+        }
134
+    }
135
+    
136
+    private var betCountSection: some View {
137
+        VStack(alignment: .leading, spacing: 16) {
138
+            SectionHeader(title: "投注设置", icon: "banknote")
139
+            
140
+            // 金额显示卡片
141
+            VStack(spacing: 12) {
142
+                HStack {
143
+                    VStack(alignment: .leading, spacing: 4) {
144
+                        Text("投注金额")
145
+                            .font(.caption)
146
+                            .foregroundColor(.secondary)
147
+                        
148
+                        Text(Formatters.formatCurrency(totalAmount))
149
+                            .font(.system(size: 28, weight: .bold, design: .rounded))
150
+                            .foregroundColor(.blue)
151
+                    }
152
+                    
153
+                    Spacer()
154
+                    
155
+                    VStack(alignment: .trailing, spacing: 4) {
156
+                        Text("倍数")
157
+                            .font(.caption)
158
+                            .foregroundColor(.secondary)
159
+                        
160
+                        Text("\(betCount)倍")
161
+                            .font(.system(size: 20, weight: .semibold, design: .rounded))
162
+                            .foregroundColor(.primary)
163
+                    }
164
+                }
165
+                
166
+                // 滑块选择器
167
+                VStack(spacing: 8) {
168
+                    HStack {
169
+                        Text("1倍")
170
+                            .font(.caption)
171
+                            .foregroundColor(.secondary)
172
+                        
173
+                        Slider(
174
+                            value: Binding(
175
+                                get: { Double(betCount) },
176
+                                set: { betCount = Int($0) }
177
+                            ),
178
+                            in: 1...50,
179
+                            step: 1
180
+                        )
181
+                        .accentColor(.blue)
182
+                        
183
+                        Text("50倍")
184
+                            .font(.caption)
185
+                            .foregroundColor(.secondary)
186
+                    }
187
+                    
188
+                    // 快速倍数
189
+                    ScrollView(.horizontal, showsIndicators: false) {
190
+                        HStack(spacing: 10) {
191
+                            ForEach(betCountOptions, id: \.self) { count in
192
+                                ChipButton(
193
+                                    title: "\(count)倍",
194
+                                    isSelected: betCount == count,
195
+                                    action: { withAnimation { betCount = count } }
196
+                                )
197
+                            }
198
+                        }
199
+                        .padding(.horizontal, 2)
200
+                    }
201
+                }
202
+            }
203
+            .padding(20)
204
+            .background(
205
+                LinearGradient(
206
+                    colors: [
207
+                        Color.blue.opacity(0.03),
208
+                        Color.blue.opacity(0.01)
209
+                    ],
210
+                    startPoint: .topLeading,
211
+                    endPoint: .bottomTrailing
212
+                )
213
+            )
214
+            .cornerRadius(16)
215
+            .overlay(
216
+                RoundedRectangle(cornerRadius: 16)
217
+                    .stroke(Color.blue.opacity(0.1), lineWidth: 1)
218
+            )
219
+        }
220
+    }
221
+    
222
+    
223
+    // MARK: - 方法
224
+    
225
+    // 生成示例号码
226
+    private func generateSampleNumbers(for type: LotteryType) -> String {
227
+        switch type {
228
+        case .doubleColorBall:
229
+            let redBalls = (1...33).shuffled().prefix(6).map { String(format: "%02d", $0) }
230
+            let blueBall = String(format: "%02d", Int.random(in: 1...16))
231
+            return "\(redBalls.joined(separator: " ")) + \(blueBall)"
232
+            
233
+        case .superLotto:
234
+            let frontBalls = (1...35).shuffled().prefix(5).map { String(format: "%02d", $0) }
235
+            let backBalls = (1...12).shuffled().prefix(2).map { String(format: "%02d", $0) }
236
+            return "\(frontBalls.joined(separator: " ")) + \(backBalls.joined(separator: " "))"
237
+            
238
+        case .happy8:
239
+            let numbers = (0...9).shuffled().prefix(3).map { String($0) }
240
+            return numbers.joined(separator: " ")
241
+            
242
+        case .sevenStar:
243
+            let numbers = (0...9).shuffled().prefix(7).map { String($0) }
244
+            return numbers.joined(separator: " ")
245
+            
246
+        case .other:
247
+            return "随机号码"
248
+        }
249
+    }
250
+    
251
+}

+ 1
- 1
LotteryTracker/Views/Home/TicketDetailView.swift Voir le fichier

@@ -107,7 +107,7 @@ struct TicketDetailView: View {
107 107
         switch ticket.lotteryType {
108 108
         case .doubleColorBall: return .red
109 109
         case .superLotto: return .blue
110
-        case .welfare3D: return .purple
110
+        case .happy8: return .purple
111 111
         case .sevenStar: return .green
112 112
         case .other: return .pink
113 113
         }

+ 2
- 2
LotteryTracker/Views/Home/TicketRow.swift Voir le fichier

@@ -87,7 +87,7 @@ struct TicketRow: View {
87 87
             return "circle.grid.2x2.fill"
88 88
         case .superLotto:
89 89
             return "8.circle.fill"
90
-        case .welfare3D:
90
+        case .happy8:
91 91
             return "3.circle.fill"
92 92
         case .sevenStar:
93 93
             return "star.fill"
@@ -102,7 +102,7 @@ struct TicketRow: View {
102 102
             return .red
103 103
         case .superLotto:
104 104
             return .blue
105
-        case .welfare3D:
105
+        case .happy8:
106 106
             return .green
107 107
         case .sevenStar:
108 108
             return .purple

+ 1
- 1
LotteryTracker/Views/Statistics/StatisticsView.swift Voir le fichier

@@ -329,7 +329,7 @@ struct TypeDistributionDetailView: View {
329 329
         switch type {
330 330
         case .doubleColorBall: return .red
331 331
         case .superLotto: return .blue
332
-        case .welfare3D: return .green
332
+        case .happy8: return .green
333 333
         case .sevenStar: return .purple
334 334
         case .other: return .gray
335 335
         }