9893d5408a
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
152 lines
6.2 KiB
Swift
152 lines
6.2 KiB
Swift
import UIKit
|
||
|
||
final class QuizResultView: UIView {
|
||
private let total: Int
|
||
private let wrongWords: [Word]
|
||
private let duration: TimeInterval
|
||
private let onRestart: () -> Void
|
||
private let onExit: () -> Void
|
||
private let onSelectWord: (Word) -> Void
|
||
|
||
private var correctCount: Int { total - wrongWords.count }
|
||
|
||
private var durationText: String {
|
||
let seconds = Int(duration)
|
||
return seconds >= 60 ? "\(seconds / 60) 分 \(seconds % 60) 秒" : "\(seconds) 秒"
|
||
}
|
||
|
||
init(total: Int, wrongWords: [Word], duration: TimeInterval,
|
||
onRestart: @escaping () -> Void, onExit: @escaping () -> Void,
|
||
onSelectWord: @escaping (Word) -> Void) {
|
||
self.total = total
|
||
self.wrongWords = wrongWords
|
||
self.duration = duration
|
||
self.onRestart = onRestart
|
||
self.onExit = onExit
|
||
self.onSelectWord = onSelectWord
|
||
super.init(frame: .zero)
|
||
backgroundColor = .systemBackground
|
||
buildLayout()
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) { fatalError() }
|
||
|
||
private func buildLayout() {
|
||
let scrollView = UIScrollView()
|
||
addSubview(scrollView)
|
||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||
NSLayoutConstraint.activate([
|
||
scrollView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
|
||
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||
scrollView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||
])
|
||
|
||
let stack = UIStackView()
|
||
stack.axis = .vertical
|
||
stack.spacing = 24
|
||
scrollView.addSubview(stack)
|
||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||
NSLayoutConstraint.activate([
|
||
stack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 24),
|
||
stack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 16),
|
||
stack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -16),
|
||
stack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -24),
|
||
stack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32)
|
||
])
|
||
|
||
let scoreLabel = UILabel()
|
||
scoreLabel.text = "\(correctCount) / \(total)"
|
||
scoreLabel.font = .systemFont(ofSize: 44, weight: .bold)
|
||
scoreLabel.textAlignment = .center
|
||
|
||
let durationLabel = UILabel()
|
||
durationLabel.text = "用时 \(durationText)"
|
||
durationLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||
durationLabel.textColor = .secondaryLabel
|
||
durationLabel.textAlignment = .center
|
||
|
||
let scoreStack = UIStackView(arrangedSubviews: [scoreLabel, durationLabel])
|
||
scoreStack.axis = .vertical
|
||
scoreStack.spacing = 8
|
||
stack.addArrangedSubview(scoreStack)
|
||
|
||
if !wrongWords.isEmpty {
|
||
stack.addArrangedSubview(makeWrongWordsSection())
|
||
}
|
||
|
||
let restartButton = makeButton(title: "再来一轮", prominent: true) { [weak self] in
|
||
self?.onRestart()
|
||
}
|
||
let exitButton = makeButton(title: "返回", prominent: false) { [weak self] in
|
||
self?.onExit()
|
||
}
|
||
let buttonStack = UIStackView(arrangedSubviews: [restartButton, exitButton])
|
||
buttonStack.axis = .vertical
|
||
buttonStack.spacing = 12
|
||
stack.addArrangedSubview(buttonStack)
|
||
}
|
||
|
||
private func makeWrongWordsSection() -> UIView {
|
||
let titleLabel = UILabel()
|
||
titleLabel.text = "错题(\(wrongWords.count))"
|
||
titleLabel.font = .preferredFont(forTextStyle: .headline)
|
||
|
||
let section = UIStackView(arrangedSubviews: [titleLabel])
|
||
section.axis = .vertical
|
||
section.spacing = 8
|
||
|
||
for word in wrongWords {
|
||
let button = UIButton(type: .system)
|
||
var config = UIButton.Configuration.plain()
|
||
config.contentInsets = .init(top: 8, leading: 12, bottom: 8, trailing: 12)
|
||
button.configuration = config
|
||
button.contentHorizontalAlignment = .fill
|
||
|
||
let wordLabel = UILabel()
|
||
wordLabel.text = word.word
|
||
wordLabel.font = .preferredFont(forTextStyle: .headline)
|
||
let meaningLabel = UILabel()
|
||
meaningLabel.text = word.meaning
|
||
meaningLabel.textColor = .secondaryLabel
|
||
let chevron = UIImageView(image: UIImage(systemName: "chevron.right"))
|
||
chevron.tintColor = .tertiaryLabel
|
||
chevron.setContentHuggingPriority(.required, for: .horizontal)
|
||
|
||
let row = UIStackView(arrangedSubviews: [wordLabel, UIView(), meaningLabel, chevron])
|
||
row.axis = .horizontal
|
||
row.alignment = .center
|
||
row.spacing = 8
|
||
row.isUserInteractionEnabled = false
|
||
button.addSubview(row)
|
||
row.translatesAutoresizingMaskIntoConstraints = false
|
||
NSLayoutConstraint.activate([
|
||
row.topAnchor.constraint(equalTo: button.topAnchor, constant: 8),
|
||
row.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: 12),
|
||
row.trailingAnchor.constraint(equalTo: button.trailingAnchor, constant: -12),
|
||
row.bottomAnchor.constraint(equalTo: button.bottomAnchor, constant: -8)
|
||
])
|
||
|
||
button.addAction(UIAction { [weak self] _ in
|
||
self?.onSelectWord(word)
|
||
}, for: .touchUpInside)
|
||
section.addArrangedSubview(button)
|
||
|
||
let divider = HairlineView()
|
||
section.addArrangedSubview(divider)
|
||
}
|
||
return section
|
||
}
|
||
|
||
private func makeButton(title: String, prominent: Bool, action: @escaping () -> Void) -> UIButton {
|
||
let button = UIButton(type: .system)
|
||
var config = prominent ? UIButton.Configuration.borderedProminent() : UIButton.Configuration.plain()
|
||
config.title = title
|
||
config.contentInsets = .init(top: 12, leading: 16, bottom: 12, trailing: 16)
|
||
button.configuration = config
|
||
button.addAction(UIAction { _ in action() }, for: .touchUpInside)
|
||
return button
|
||
}
|
||
}
|