fix: 优化任务详情与图片预览布局

This commit is contained in:
2026-08-18 16:41:55 +08:00
parent c34c4e1923
commit 1757ba5e36
3 changed files with 237 additions and 74 deletions
@@ -533,7 +533,7 @@ private final class AIJobDetailAlbumCard: UIView {
}
}
/// 任务内容卡,以浅蓝标签展示各输出目标数量并补充额度结算。
/// 任务内容卡,以三色紧凑标签展示各输出目标数量并补充额度结算。
private final class AIJobDetailContentCard: UIView {
private let outputStack = UIStackView()
private let quotaLabel = UILabel()
@@ -548,7 +548,8 @@ private final class AIJobDetailContentCard: UIView {
titleLabel.textColor = AIJobDetailStyle.textPrimary
outputStack.axis = .horizontal
outputStack.spacing = 10
outputStack.distribution = .fillEqually
outputStack.distribution = .fill
outputStack.alignment = .center
quotaLabel.font = .systemFont(ofSize: 12)
quotaLabel.textColor = AIJobDetailStyle.textSecondary
quotaLabel.numberOfLines = 0
@@ -565,6 +566,12 @@ private final class AIJobDetailContentCard: UIView {
func apply(_ detail: TravelAlbumAIJobDetail) {
outputStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
detail.outputs.forEach { outputStack.addArrangedSubview(makeChip($0)) }
if let lastChip = outputStack.arrangedSubviews.last {
let spacer = UIView()
spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
outputStack.setCustomSpacing(0, after: lastChip)
outputStack.addArrangedSubview(spacer)
}
outputStack.isHidden = detail.outputs.isEmpty
let quota = detail.quotaSettlement
quotaLabel.text = "额度:预占 \(quota.reservedUnits) · 消耗 \(quota.consumedUnits) · 释放 \(quota.releasedUnits)"
@@ -573,32 +580,42 @@ private final class AIJobDetailContentCard: UIView {
}
private func makeChip(_ output: TravelAlbumAIJobOutput) -> UIView {
let container = UIView()
container.backgroundColor = AppColor.primaryLight
container.layer.cornerRadius = 9
let iconView = UIImageView(image: UIImage(systemName: output.type.symbolName))
iconView.tintColor = AppColor.primary
iconView.contentMode = .scaleAspectFit
let label = UILabel()
label.text = "\(output.type.shortTitle) \(output.count) 张"
AIJobDetailOutputChip(
text: "\(output.type.shortTitle) \(output.count)张",
backgroundColor: output.type.chipBackgroundColor,
accessibilityIdentifier: "aiRetouchJob.contentChip.\(output.type.chipIdentifier)"
)
}
}
/// 依据文字固有宽度展示的任务输出类型标签。
private final class AIJobDetailOutputChip: UIView {
private let label = UILabel()
init(text: String, backgroundColor: UIColor, accessibilityIdentifier: String) {
super.init(frame: .zero)
self.backgroundColor = backgroundColor
self.accessibilityIdentifier = accessibilityIdentifier
layer.cornerRadius = 9
setContentHuggingPriority(.required, for: .horizontal)
setContentCompressionResistancePriority(.required, for: .horizontal)
label.text = text
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = AIJobDetailStyle.textPrimary
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.75
container.addSubview(iconView)
container.addSubview(label)
iconView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(10)
make.centerY.equalToSuperview()
make.size.equalTo(18)
}
label.setContentCompressionResistancePriority(.required, for: .horizontal)
addSubview(label)
label.snp.makeConstraints { make in
make.leading.equalTo(iconView.snp.trailing).offset(6)
make.trailing.equalToSuperview().inset(8)
make.leading.trailing.equalToSuperview().inset(12)
make.centerY.equalToSuperview()
}
container.snp.makeConstraints { $0.height.equalTo(44) }
return container
snp.makeConstraints { $0.height.equalTo(44) }
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override var intrinsicContentSize: CGSize {
CGSize(width: ceil(label.intrinsicContentSize.width) + 24, height: 44)
}
}
@@ -663,6 +680,9 @@ private final class AIJobDetailTargetRow: UIView {
private let templateLabel = UILabel()
private let statusIconView = UIImageView()
private let statusLabel = UILabel()
private let statusStack = UIStackView()
private let statusRow = UIStackView()
private let contentStack = UIStackView()
private let resultButton = UIButton(type: .system)
private let errorContainer = UIView()
private let errorIconView = UIImageView(image: UIImage(systemName: "exclamationmark.circle.fill"))
@@ -679,8 +699,11 @@ private final class AIJobDetailTargetRow: UIView {
titleLabel.font = .systemFont(ofSize: 15, weight: .medium)
titleLabel.textColor = AIJobDetailStyle.textPrimary
titleLabel.lineBreakMode = .byTruncatingMiddle
titleLabel.numberOfLines = 1
templateLabel.font = .systemFont(ofSize: 13)
templateLabel.textColor = AIJobDetailStyle.textSecondary
templateLabel.lineBreakMode = .byTruncatingTail
templateLabel.numberOfLines = 1
statusIconView.contentMode = .scaleAspectFit
statusLabel.font = .systemFont(ofSize: 14, weight: .medium)
resultButton.setTitle("查看结果", for: .normal)
@@ -693,49 +716,45 @@ private final class AIJobDetailTargetRow: UIView {
errorLabel.textColor = AppColor.danger
errorLabel.numberOfLines = 0
let statusStack = UIStackView(arrangedSubviews: [statusIconView, statusLabel])
statusStack.addArrangedSubview(statusIconView)
statusStack.addArrangedSubview(statusLabel)
statusStack.axis = .horizontal
statusStack.spacing = 6
statusStack.alignment = .center
statusStack.setContentCompressionResistancePriority(.required, for: .horizontal)
statusLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
let statusSpacer = UIView()
statusRow.addArrangedSubview(statusStack)
statusRow.addArrangedSubview(statusSpacer)
statusRow.addArrangedSubview(resultButton)
statusRow.axis = .horizontal
statusRow.spacing = 8
statusRow.alignment = .center
contentStack.addArrangedSubview(titleLabel)
contentStack.addArrangedSubview(templateLabel)
contentStack.addArrangedSubview(statusRow)
contentStack.addArrangedSubview(errorContainer)
contentStack.axis = .vertical
contentStack.spacing = 0
contentStack.setCustomSpacing(7, after: titleLabel)
contentStack.setCustomSpacing(8, after: templateLabel)
contentStack.setCustomSpacing(12, after: statusRow)
statusIconView.snp.makeConstraints { $0.size.equalTo(18) }
addSubview(thumbnailView)
addSubview(titleLabel)
addSubview(templateLabel)
addSubview(statusStack)
addSubview(resultButton)
addSubview(errorContainer)
addSubview(contentStack)
errorContainer.addSubview(errorIconView)
errorContainer.addSubview(errorLabel)
thumbnailView.snp.makeConstraints { make in
make.leading.top.equalToSuperview().offset(4)
make.size.equalTo(80)
make.bottom.lessThanOrEqualToSuperview().inset(12)
}
titleLabel.snp.makeConstraints { make in
contentStack.snp.makeConstraints { make in
make.leading.equalTo(thumbnailView.snp.trailing).offset(14)
make.top.equalTo(thumbnailView).offset(18)
make.trailing.lessThanOrEqualTo(statusStack.snp.leading).offset(-8)
}
templateLabel.snp.makeConstraints { make in
make.leading.equalTo(titleLabel)
make.top.equalTo(titleLabel.snp.bottom).offset(7)
make.trailing.lessThanOrEqualTo(statusStack.snp.leading).offset(-8)
}
statusStack.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(4)
make.centerY.equalTo(thumbnailView).offset(-7)
}
resultButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(4)
make.top.equalTo(statusStack.snp.bottom).offset(4)
make.height.equalTo(28)
}
errorContainer.snp.makeConstraints { make in
make.top.equalTo(templateLabel.snp.bottom).offset(12)
make.leading.equalTo(titleLabel)
make.trailing.equalToSuperview().inset(4)
make.bottom.equalToSuperview().inset(12)
make.height.greaterThanOrEqualTo(38)
}
errorIconView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(10)
@@ -780,21 +799,9 @@ private final class AIJobDetailTargetRow: UIView {
errorLabel.text = target.displayFailureMessage.map { "失败原因:\($0)" }
let showsError = target.displayFailureMessage != nil
errorContainer.isHidden = !showsError
errorContainer.snp.remakeConstraints { make in
if showsError {
make.top.equalTo(templateLabel.snp.bottom).offset(12)
} else {
make.top.equalTo(thumbnailView.snp.bottom)
}
make.leading.equalTo(titleLabel)
make.trailing.equalToSuperview().inset(4)
make.bottom.equalToSuperview().inset(12)
if showsError {
make.height.greaterThanOrEqualTo(38)
} else {
make.height.equalTo(0)
}
}
titleLabel.accessibilityIdentifier = "aiRetouchJob.target.\(target.targetId).title"
templateLabel.accessibilityIdentifier = "aiRetouchJob.target.\(target.targetId).template"
statusStack.accessibilityIdentifier = "aiRetouchJob.target.\(target.targetId).status"
accessibilityLabel = [titleLabel.text, templateLabel.text, statusLabel.text, errorLabel.text]
.compactMap { $0 }.joined(separator: ",")
}
@@ -896,12 +903,21 @@ private extension TravelAlbumAIJobOutputType {
}
}
var symbolName: String {
var chipBackgroundColor: UIColor {
switch self {
case .refined: "wand.and.stars"
case .atmosphere: "sun.max"
case .cover: "bookmark"
case .unknown: "sparkles"
case .refined: UIColor(hex: 0xEAF3FF)
case .atmosphere: UIColor(hex: 0xFFF2DC)
case .cover: UIColor(hex: 0xF2ECFF)
case .unknown: UIColor(hex: 0xF1F5F9)
}
}
var chipIdentifier: String {
switch self {
case .refined: "refined"
case .atmosphere: "atmosphere"
case .cover: "cover"
case .unknown: "unknown"
}
}
}