完善账号切换展示、首页菜单图标与有线传输设置 chip。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-15 14:59:31 +08:00
parent 5eef31b8da
commit ce6de76055
18 changed files with 321 additions and 130 deletions

View File

@ -4,6 +4,14 @@
"filename" : "home_menu_space.svg", "filename" : "home_menu_space.svg",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {

View File

@ -4,6 +4,14 @@
"filename" : "profile_avatar_default.svg", "filename" : "profile_avatar_default.svg",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {

View File

@ -4,6 +4,14 @@
"filename" : "profile_edit.svg", "filename" : "profile_edit.svg",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {

View File

@ -118,6 +118,7 @@ struct AccountSwitchAccount: Identifiable, Hashable {
let title: String let title: String
let subtitle: String let subtitle: String
let phone: String let phone: String
let realName: String
let avatar: String let avatar: String
let scenicName: String let scenicName: String
let storeId: Int? let storeId: Int?
@ -201,6 +202,7 @@ struct V9ScenicUser: Decodable, Equatable {
let phone: String let phone: String
let scenicId: Int let scenicId: Int
let scenicName: String let scenicName: String
let roleName: String
let appRoleCode: String let appRoleCode: String
let appRoleName: String let appRoleName: String
let isCurrent: Bool let isCurrent: Bool
@ -218,8 +220,9 @@ struct V9ScenicUser: Decodable, Equatable {
accountType: accountType.nonEmpty ?? Self.accountTypeValue, accountType: accountType.nonEmpty ?? Self.accountTypeValue,
businessUserId: businessUserId, businessUserId: businessUserId,
title: scenicName.nonEmpty ?? nickname.nonEmpty ?? realName.nonEmpty ?? "景区账号", title: scenicName.nonEmpty ?? nickname.nonEmpty ?? realName.nonEmpty ?? "景区账号",
subtitle: username, subtitle: joinAccountSubtitle(scenicName, roleName),
phone: phone, phone: phone,
realName: realName,
avatar: "", avatar: "",
scenicName: scenicName, scenicName: scenicName,
storeId: nil, storeId: nil,
@ -242,6 +245,7 @@ struct V9ScenicUser: Decodable, Equatable {
case phone case phone
case scenicId = "scenic_id" case scenicId = "scenic_id"
case scenicName = "scenic_name" case scenicName = "scenic_name"
case roleName = "role_name"
case appRoleCode = "app_role_code" case appRoleCode = "app_role_code"
case appRoleName = "app_role_name" case appRoleName = "app_role_name"
case isCurrent = "is_current" case isCurrent = "is_current"
@ -262,6 +266,7 @@ struct V9ScenicUser: Decodable, Equatable {
phone = try container.decodeLossyString(forKey: .phone) phone = try container.decodeLossyString(forKey: .phone)
scenicId = try container.decodeLossyInt(forKey: .scenicId) ?? 0 scenicId = try container.decodeLossyInt(forKey: .scenicId) ?? 0
scenicName = try container.decodeLossyString(forKey: .scenicName) scenicName = try container.decodeLossyString(forKey: .scenicName)
roleName = try container.decodeLossyString(forKey: .roleName)
appRoleCode = try container.decodeLossyString(forKey: .appRoleCode) appRoleCode = try container.decodeLossyString(forKey: .appRoleCode)
appRoleName = try container.decodeLossyString(forKey: .appRoleName) appRoleName = try container.decodeLossyString(forKey: .appRoleName)
isCurrent = try container.decodeLossyBool(forKey: .isCurrent) ?? false isCurrent = try container.decodeLossyBool(forKey: .isCurrent) ?? false
@ -285,6 +290,7 @@ struct V9StoreUser: Decodable, Equatable {
let scenicName: String let scenicName: String
let storeId: Int let storeId: Int
let storeName: String let storeName: String
let roleName: String
let appRoleCode: String let appRoleCode: String
let appRoleName: String let appRoleName: String
let isCurrent: Bool let isCurrent: Bool
@ -302,8 +308,9 @@ struct V9StoreUser: Decodable, Equatable {
accountType: accountType.nonEmpty ?? Self.accountTypeValue, accountType: accountType.nonEmpty ?? Self.accountTypeValue,
businessUserId: businessUserId, businessUserId: businessUserId,
title: storeName.nonEmpty ?? scenicName.nonEmpty ?? realName.nonEmpty ?? userName.nonEmpty ?? "门店账号", title: storeName.nonEmpty ?? scenicName.nonEmpty ?? realName.nonEmpty ?? userName.nonEmpty ?? "门店账号",
subtitle: userName.nonEmpty ?? username, subtitle: joinAccountSubtitle(scenicName, roleName),
phone: phone, phone: phone,
realName: realName,
avatar: avatar, avatar: avatar,
scenicName: scenicName, scenicName: scenicName,
storeId: storeId > 0 ? storeId : nil, storeId: storeId > 0 ? storeId : nil,
@ -327,6 +334,7 @@ struct V9StoreUser: Decodable, Equatable {
case scenicName = "scenic_name" case scenicName = "scenic_name"
case storeId = "store_id" case storeId = "store_id"
case storeName = "store_name" case storeName = "store_name"
case roleName = "role_name"
case appRoleCode = "app_role_code" case appRoleCode = "app_role_code"
case appRoleName = "app_role_name" case appRoleName = "app_role_name"
case isCurrent = "is_current" case isCurrent = "is_current"
@ -347,6 +355,7 @@ struct V9StoreUser: Decodable, Equatable {
scenicName = try container.decodeLossyString(forKey: .scenicName) scenicName = try container.decodeLossyString(forKey: .scenicName)
storeId = try container.decodeLossyInt(forKey: .storeId) ?? 0 storeId = try container.decodeLossyInt(forKey: .storeId) ?? 0
storeName = try container.decodeLossyString(forKey: .storeName) storeName = try container.decodeLossyString(forKey: .storeName)
roleName = try container.decodeLossyString(forKey: .roleName)
appRoleCode = try container.decodeLossyString(forKey: .appRoleCode) appRoleCode = try container.decodeLossyString(forKey: .appRoleCode)
appRoleName = try container.decodeLossyString(forKey: .appRoleName) appRoleName = try container.decodeLossyString(forKey: .appRoleName)
isCurrent = try container.decodeLossyBool(forKey: .isCurrent) ?? false isCurrent = try container.decodeLossyBool(forKey: .isCurrent) ?? false
@ -405,6 +414,14 @@ private extension KeyedDecodingContainer {
} }
} }
///
private func joinAccountSubtitle(_ values: String...) -> String {
values
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
.joined(separator: " · ")
}
private extension String { private extension String {
var nonEmpty: String? { var nonEmpty: String? {
let text = trimmingCharacters(in: .whitespacesAndNewlines) let text = trimmingCharacters(in: .whitespacesAndNewlines)

View File

@ -0,0 +1,21 @@
//
// HomeMenuIconFactory.swift
// suixinkan
//
import UIKit
/// / SF Symbol
enum HomeMenuIconFactory {
/// 2426pt
static let symbolConfiguration = UIImage.SymbolConfiguration(pointSize: 18, weight: .medium)
/// Assets SF Symbol
static func image(named iconName: String) -> UIImage? {
if let asset = UIImage(named: iconName) {
return asset
}
return UIImage(systemName: iconName, withConfiguration: symbolConfiguration)
}
}

View File

@ -42,10 +42,11 @@ final class AllFunctionMenuCell: UICollectionViewCell {
/// ///
func apply(menu: HomeMenuItem, actionStyle: ActionStyle) { func apply(menu: HomeMenuItem, actionStyle: ActionStyle) {
iconView.image = UIImage(named: menu.iconName) ?? UIImage(systemName: menu.iconName) iconView.image = HomeMenuIconFactory.image(named: menu.iconName)
titleLabel.text = menu.title titleLabel.text = menu.title
accessibilityLabel = menu.title accessibilityLabel = menu.title
switch actionStyle { switch actionStyle {
case .none: case .none:
actionImageView.isHidden = true actionImageView.isHidden = true
@ -75,6 +76,8 @@ final class AllFunctionMenuCell: UICollectionViewCell {
iconView.tintColor = AppColor.primary iconView.tintColor = AppColor.primary
iconView.contentMode = .scaleAspectFit iconView.contentMode = .scaleAspectFit
iconView.preferredSymbolConfiguration = HomeMenuIconFactory.symbolConfiguration
contentStackView.axis = .vertical contentStackView.axis = .vertical
contentStackView.alignment = .center contentStackView.alignment = .center

View File

@ -128,7 +128,7 @@ final class HomeMenuCell: UICollectionViewCell {
} }
func apply(menu: HomeMenuItem) { func apply(menu: HomeMenuItem) {
iconView.image = UIImage(named: menu.iconName) ?? UIImage(systemName: menu.iconName) iconView.image = HomeMenuIconFactory.image(named: menu.iconName)
iconView.accessibilityIdentifier = menu.iconName iconView.accessibilityIdentifier = menu.iconName
titleLabel.text = menu.title titleLabel.text = menu.title
} }
@ -147,6 +147,8 @@ final class HomeMenuCell: UICollectionViewCell {
iconView.tintColor = AppColor.primary iconView.tintColor = AppColor.primary
iconView.contentMode = .scaleAspectFit iconView.contentMode = .scaleAspectFit
iconView.preferredSymbolConfiguration = HomeMenuIconFactory.symbolConfiguration
contentStackView.axis = .vertical contentStackView.axis = .vertical
contentStackView.alignment = .center contentStackView.alignment = .center

View File

@ -186,7 +186,7 @@ private final class AccountSelectionCell: UITableViewCell {
private let avatarPlaceholderLabel = UILabel() private let avatarPlaceholderLabel = UILabel()
private let titleLabel = UILabel() private let titleLabel = UILabel()
private let subtitleLabel = UILabel() private let subtitleLabel = UILabel()
private let phoneLabel = UILabel() private let nameLabel = UILabel()
private let typeTag = UILabel() private let typeTag = UILabel()
private let currentTag = UILabel() private let currentTag = UILabel()
private let checkmark = UIImageView() private let checkmark = UIImageView()
@ -214,8 +214,8 @@ private final class AccountSelectionCell: UITableViewCell {
subtitleLabel.font = .systemFont(ofSize: 13) subtitleLabel.font = .systemFont(ofSize: 13)
subtitleLabel.textColor = AppColor.text666 subtitleLabel.textColor = AppColor.text666
phoneLabel.font = .systemFont(ofSize: 12) nameLabel.font = .systemFont(ofSize: 12)
phoneLabel.textColor = UIColor(hex: 0x9AA1AA) nameLabel.textColor = UIColor(hex: 0x9AA1AA)
typeTag.font = .systemFont(ofSize: 11, weight: .semibold) typeTag.font = .systemFont(ofSize: 11, weight: .semibold)
typeTag.textAlignment = .center typeTag.textAlignment = .center
@ -239,7 +239,7 @@ private final class AccountSelectionCell: UITableViewCell {
avatarView.addSubview(avatarPlaceholderLabel) avatarView.addSubview(avatarPlaceholderLabel)
card.addSubview(titleLabel) card.addSubview(titleLabel)
card.addSubview(subtitleLabel) card.addSubview(subtitleLabel)
card.addSubview(phoneLabel) card.addSubview(nameLabel)
card.addSubview(typeTag) card.addSubview(typeTag)
card.addSubview(currentTag) card.addSubview(currentTag)
card.addSubview(checkmark) card.addSubview(checkmark)
@ -277,7 +277,7 @@ private final class AccountSelectionCell: UITableViewCell {
make.trailing.equalTo(checkmark.snp.leading).offset(-8) make.trailing.equalTo(checkmark.snp.leading).offset(-8)
} }
phoneLabel.snp.makeConstraints { make in nameLabel.snp.makeConstraints { make in
make.top.equalTo(subtitleLabel.snp.bottom).offset(4) make.top.equalTo(subtitleLabel.snp.bottom).offset(4)
make.leading.equalTo(titleLabel) make.leading.equalTo(titleLabel)
make.bottom.equalToSuperview().inset(16) make.bottom.equalToSuperview().inset(16)
@ -304,8 +304,8 @@ private final class AccountSelectionCell: UITableViewCell {
titleLabel.text = account.title.isEmpty ? account.accountTypeLabel : account.title titleLabel.text = account.title.isEmpty ? account.accountTypeLabel : account.title
subtitleLabel.text = account.subtitle subtitleLabel.text = account.subtitle
subtitleLabel.isHidden = account.subtitle.isEmpty subtitleLabel.isHidden = account.subtitle.isEmpty
phoneLabel.text = account.phone nameLabel.text = account.realName
phoneLabel.isHidden = account.phone.isEmpty nameLabel.isHidden = account.realName.isEmpty
currentTag.isHidden = !account.isCurrent currentTag.isHidden = !account.isCurrent
let avatarPlaceholderColor: UIColor let avatarPlaceholderColor: UIColor

View File

@ -161,7 +161,7 @@ private final class AccountSwitchCell: UITableViewCell {
private let titleLabel = UILabel() private let titleLabel = UILabel()
private let currentTagLabel = UILabel() private let currentTagLabel = UILabel()
private let subtitleLabel = UILabel() private let subtitleLabel = UILabel()
private let phoneLabel = UILabel() private let nameLabel = UILabel()
private let tagLabel = UILabel() private let tagLabel = UILabel()
private let checkView = UIImageView() private let checkView = UIImageView()
@ -197,8 +197,8 @@ private final class AccountSwitchCell: UITableViewCell {
currentTagLabel.setContentCompressionResistancePriority(.required, for: .horizontal) currentTagLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
subtitleLabel.font = .systemFont(ofSize: 13) subtitleLabel.font = .systemFont(ofSize: 13)
subtitleLabel.textColor = AppColor.text666 subtitleLabel.textColor = AppColor.text666
phoneLabel.font = .systemFont(ofSize: 12) nameLabel.font = .systemFont(ofSize: 12)
phoneLabel.textColor = UIColor(hex: 0x9AA1AA) nameLabel.textColor = UIColor(hex: 0x9AA1AA)
tagLabel.font = .systemFont(ofSize: 11, weight: .semibold) tagLabel.font = .systemFont(ofSize: 11, weight: .semibold)
tagLabel.textAlignment = .center tagLabel.textAlignment = .center
tagLabel.layer.cornerRadius = 4 tagLabel.layer.cornerRadius = 4
@ -210,7 +210,7 @@ private final class AccountSwitchCell: UITableViewCell {
card.addSubview(titleStackView) card.addSubview(titleStackView)
card.addSubview(subtitleLabel) card.addSubview(subtitleLabel)
card.addSubview(phoneLabel) card.addSubview(nameLabel)
card.addSubview(tagLabel) card.addSubview(tagLabel)
card.addSubview(checkView) card.addSubview(checkView)
@ -234,7 +234,7 @@ private final class AccountSwitchCell: UITableViewCell {
make.leading.equalTo(titleStackView) make.leading.equalTo(titleStackView)
make.top.equalTo(titleStackView.snp.bottom).offset(4) make.top.equalTo(titleStackView.snp.bottom).offset(4)
} }
phoneLabel.snp.makeConstraints { make in nameLabel.snp.makeConstraints { make in
make.leading.equalTo(titleLabel) make.leading.equalTo(titleLabel)
make.top.equalTo(subtitleLabel.snp.bottom).offset(4) make.top.equalTo(subtitleLabel.snp.bottom).offset(4)
} }
@ -257,7 +257,9 @@ private final class AccountSwitchCell: UITableViewCell {
titleLabel.text = account.title titleLabel.text = account.title
currentTagLabel.isHidden = !isCurrent currentTagLabel.isHidden = !isCurrent
subtitleLabel.text = account.subtitle subtitleLabel.text = account.subtitle
phoneLabel.text = account.phone subtitleLabel.isHidden = account.subtitle.isEmpty
nameLabel.text = account.realName
nameLabel.isHidden = account.realName.isEmpty
tagLabel.text = account.isStoreUser ? "门店" : "景区" tagLabel.text = account.isStoreUser ? "门店" : "景区"
tagLabel.textColor = account.isStoreUser ? UIColor(hex: 0x0F9F6E) : UIColor(hex: 0x7C3AED) tagLabel.textColor = account.isStoreUser ? UIColor(hex: 0x0F9F6E) : UIColor(hex: 0x7C3AED)
tagLabel.backgroundColor = account.isStoreUser ? UIColor(hex: 0xE8F8F1) : UIColor(hex: 0xF3ECFF) tagLabel.backgroundColor = account.isStoreUser ? UIColor(hex: 0xE8F8F1) : UIColor(hex: 0xF3ECFF)

View File

@ -55,7 +55,6 @@ final class TravelAlbumEntryViewController: BaseViewController {
cell.apply(album: album) cell.apply(album: album)
cell.onShootUpload = { [weak self] in self?.pushWiredTransfer(album: album) } cell.onShootUpload = { [weak self] in self?.pushWiredTransfer(album: album) }
cell.onAlbumCode = { [weak self] in self?.loadAlbumCode(album) } cell.onAlbumCode = { [weak self] in self?.loadAlbumCode(album) }
cell.onShare = { [weak self] in self?.showShareSheet() }
return cell return cell
} }
@ -162,18 +161,6 @@ final class TravelAlbumEntryViewController: BaseViewController {
present(controller, animated: true) present(controller, animated: true)
} }
private func showShareSheet() {
let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
sheet.addAction(UIAlertAction(title: "微信好友", style: .default) { [weak self] _ in
self?.showToast("已选择微信好友")
})
sheet.addAction(UIAlertAction(title: "QQ好友", style: .default) { [weak self] _ in
self?.showToast("已选择QQ好友")
})
sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
present(sheet, animated: true)
}
private func pushWiredTransfer(album: TravelAlbum) { private func pushWiredTransfer(album: TravelAlbum) {
let controller = WiredCameraTransferViewController( let controller = WiredCameraTransferViewController(
viewModel: WiredCameraTransferViewModel( viewModel: WiredCameraTransferViewModel(
@ -272,7 +259,6 @@ private final class TravelAlbumTaskCell: UITableViewCell {
var onShootUpload: (() -> Void)? var onShootUpload: (() -> Void)?
var onAlbumCode: (() -> Void)? var onAlbumCode: (() -> Void)?
var onShare: (() -> Void)?
private let cardView = UIView() private let cardView = UIView()
private let coverView = UIImageView() private let coverView = UIImageView()
@ -280,7 +266,6 @@ private final class TravelAlbumTaskCell: UITableViewCell {
private let nameLabel = UILabel() private let nameLabel = UILabel()
private let timeLabel = UILabel() private let timeLabel = UILabel()
private let phoneLabel = UILabel() private let phoneLabel = UILabel()
private let shareButton = UIButton(type: .system)
private let codeButton = UIButton(type: .system) private let codeButton = UIButton(type: .system)
private let uploadButton = UIButton(type: .system) private let uploadButton = UIButton(type: .system)
@ -314,8 +299,6 @@ private final class TravelAlbumTaskCell: UITableViewCell {
phoneLabel.numberOfLines = 1 phoneLabel.numberOfLines = 1
phoneLabel.setContentCompressionResistancePriority(.required, for: .vertical) phoneLabel.setContentCompressionResistancePriority(.required, for: .vertical)
shareButton.setImage(UIImage(systemName: "square.and.arrow.up"), for: .normal)
shareButton.tintColor = AppColor.primary
codeButton.setImage(UIImage(systemName: "qrcode"), for: .normal) codeButton.setImage(UIImage(systemName: "qrcode"), for: .normal)
codeButton.tintColor = .black codeButton.tintColor = .black
uploadButton.setImage(UIImage(systemName: "camera.fill"), for: .normal) uploadButton.setImage(UIImage(systemName: "camera.fill"), for: .normal)
@ -332,7 +315,6 @@ private final class TravelAlbumTaskCell: UITableViewCell {
cardView.addSubview(nameLabel) cardView.addSubview(nameLabel)
cardView.addSubview(timeLabel) cardView.addSubview(timeLabel)
cardView.addSubview(phoneLabel) cardView.addSubview(phoneLabel)
cardView.addSubview(shareButton)
cardView.addSubview(codeButton) cardView.addSubview(codeButton)
cardView.addSubview(uploadButton) cardView.addSubview(uploadButton)
@ -347,20 +329,15 @@ private final class TravelAlbumTaskCell: UITableViewCell {
make.center.equalToSuperview() make.center.equalToSuperview()
make.size.equalTo(42) make.size.equalTo(42)
} }
shareButton.snp.makeConstraints { make in
make.top.equalToSuperview().offset(12)
make.trailing.equalTo(codeButton.snp.leading).offset(-8)
make.size.equalTo(26)
}
codeButton.snp.makeConstraints { make in codeButton.snp.makeConstraints { make in
make.top.equalTo(shareButton) make.top.equalToSuperview().offset(12)
make.trailing.equalToSuperview().offset(-10) make.trailing.equalToSuperview().offset(-10)
make.size.equalTo(26) make.size.equalTo(26)
} }
nameLabel.snp.makeConstraints { make in nameLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(14) make.top.equalToSuperview().offset(14)
make.leading.equalTo(coverView.snp.trailing).offset(12) make.leading.equalTo(coverView.snp.trailing).offset(12)
make.trailing.equalTo(shareButton.snp.leading).offset(-8) make.trailing.equalTo(codeButton.snp.leading).offset(-8)
} }
timeLabel.snp.makeConstraints { make in timeLabel.snp.makeConstraints { make in
make.top.equalTo(nameLabel.snp.bottom).offset(7) make.top.equalTo(nameLabel.snp.bottom).offset(7)
@ -381,7 +358,6 @@ private final class TravelAlbumTaskCell: UITableViewCell {
} }
uploadButton.addTarget(self, action: #selector(uploadTapped), for: .touchUpInside) uploadButton.addTarget(self, action: #selector(uploadTapped), for: .touchUpInside)
shareButton.addTarget(self, action: #selector(shareTapped), for: .touchUpInside)
codeButton.addTarget(self, action: #selector(codeTapped), for: .touchUpInside) codeButton.addTarget(self, action: #selector(codeTapped), for: .touchUpInside)
} }
@ -404,7 +380,6 @@ private final class TravelAlbumTaskCell: UITableViewCell {
} }
@objc private func uploadTapped() { onShootUpload?() } @objc private func uploadTapped() { onShootUpload?() }
@objc private func shareTapped() { onShare?() }
@objc private func codeTapped() { onAlbumCode?() } @objc private func codeTapped() { onAlbumCode?() }
} }

View File

@ -40,10 +40,9 @@ final class WiredCameraTransferViewController: BaseViewController {
private let refreshButton = UIButton(type: .system) private let refreshButton = UIButton(type: .system)
private let helpLabel = UILabel() private let helpLabel = UILabel()
private let chipsStack = UIStackView() private let chipsStack = UIStackView()
private let retouchButton = UIButton(type: .system) private let retouchButton = WiredTransferSettingChipButton()
private let formatButton = UIButton(type: .system) private let formatButton = WiredTransferSettingChipButton()
private let modeButton = UIButton(type: .system) private let modeButton = WiredTransferSettingChipButton(showsChevron: true)
private let modeChevronView = UIImageView()
private let settingsStatsDivider = UIView() private let settingsStatsDivider = UIView()
private let statsCard = UIStackView() private let statsCard = UIStackView()
private let firstStatsDivider = UIView() private let firstStatsDivider = UIView()
@ -160,19 +159,10 @@ final class WiredCameraTransferViewController: BaseViewController {
chipsStack.spacing = 6 chipsStack.spacing = 6
chipsStack.distribution = .fillEqually chipsStack.distribution = .fillEqually
[retouchButton, formatButton, modeButton].forEach { [retouchButton, formatButton, modeButton].forEach {
configureChipButton($0)
chipsStack.addArrangedSubview($0) chipsStack.addArrangedSubview($0)
} }
retouchButton.isUserInteractionEnabled = false retouchButton.isUserInteractionEnabled = false
formatButton.isUserInteractionEnabled = false formatButton.isUserInteractionEnabled = false
modeButton.setConfigurationContentInsets(
NSDirectionalEdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 32)
)
modeChevronView.image = UIImage(systemName: "chevron.down")?
.withConfiguration(UIImage.SymbolConfiguration(pointSize: 13, weight: .semibold))
modeChevronView.tintColor = AppColor.textTertiary
modeChevronView.contentMode = .scaleAspectFit
modeChevronView.isUserInteractionEnabled = false
settingsStatsDivider.backgroundColor = AppColor.border settingsStatsDivider.backgroundColor = AppColor.border
statsCard.axis = .horizontal statsCard.axis = .horizontal
@ -269,7 +259,6 @@ final class WiredCameraTransferViewController: BaseViewController {
view.addSubview(emptyLabel) view.addSubview(emptyLabel)
view.addSubview(bottomBar) view.addSubview(bottomBar)
bottomBar.addSubview(bottomButtonsStack) bottomBar.addSubview(bottomButtonsStack)
modeButton.addSubview(modeChevronView)
bottomButtonsStack.addArrangedSubview(batchButton) bottomButtonsStack.addArrangedSubview(batchButton)
bottomButtonsStack.addArrangedSubview(specifyButton) bottomButtonsStack.addArrangedSubview(specifyButton)
bottomButtonsStack.addArrangedSubview(albumImportButton) bottomButtonsStack.addArrangedSubview(albumImportButton)
@ -318,11 +307,6 @@ final class WiredCameraTransferViewController: BaseViewController {
make.leading.trailing.equalToSuperview().inset(14) make.leading.trailing.equalToSuperview().inset(14)
make.height.equalTo(34) make.height.equalTo(34)
} }
modeChevronView.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-10)
make.centerY.equalToSuperview()
make.size.equalTo(16)
}
settingsStatsDivider.snp.makeConstraints { make in settingsStatsDivider.snp.makeConstraints { make in
make.top.equalTo(chipsStack.snp.bottom).offset(12) make.top.equalTo(chipsStack.snp.bottom).offset(12)
make.leading.trailing.equalToSuperview() make.leading.trailing.equalToSuperview()
@ -431,9 +415,9 @@ final class WiredCameraTransferViewController: BaseViewController {
statusLabel.backgroundColor = (isFailed ? AppColor.danger : AppColor.primary).withAlphaComponent(0.10) statusLabel.backgroundColor = (isFailed ? AppColor.danger : AppColor.primary).withAlphaComponent(0.10)
refreshButton.setTitle(viewModel.actionButtonText, for: .normal) refreshButton.setTitle(viewModel.actionButtonText, for: .normal)
retouchButton.setTitle(viewModel.retouchOption, for: .normal) retouchButton.apply(title: viewModel.retouchOption)
formatButton.setTitle("JPG", for: .normal) formatButton.apply(title: "JPG")
modeButton.setTitle(viewModel.transferModeOption, for: .normal) modeButton.apply(title: viewModel.transferModeOption)
helpLabel.attributedText = helpText(viewModel.sonyMTPHint) helpLabel.attributedText = helpText(viewModel.sonyMTPHint)
rebuildStats() rebuildStats()
@ -642,19 +626,6 @@ final class WiredCameraTransferViewController: BaseViewController {
statsCard.bringSubviewToFront(secondStatsDivider) statsCard.bringSubviewToFront(secondStatsDivider)
} }
private func configureChipButton(_ button: UIButton) {
button.titleLabel?.font = .systemFont(ofSize: 11)
button.setTitleColor(AppColor.textTertiary, for: .normal)
button.backgroundColor = AppColor.pageBackground
button.layer.cornerRadius = 8
button.layer.borderWidth = 1
button.layer.borderColor = AppColor.border.cgColor
button.contentHorizontalAlignment = .leading
button.setConfigurationContentInsets(
NSDirectionalEdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10)
)
}
private func makeStatButton(title: String, count: Int, selected: Bool, danger: Bool) -> UIButton { private func makeStatButton(title: String, count: Int, selected: Bool, danger: Bool) -> UIButton {
var config = UIButton.Configuration.plain() var config = UIButton.Configuration.plain()
config.title = "\(count)" config.title = "\(count)"

View File

@ -0,0 +1,88 @@
//
// WiredTransferSettingChipButton.swift
// suixinkan
//
import SnapKit
import UIKit
/// 线 chip `UIButton` title
final class WiredTransferSettingChipButton: UIButton {
private let titleLabelView = UILabel()
private let chevronView = UIImageView()
private var titleTrailingToSuperviewConstraint: Constraint?
private var titleTrailingToChevronConstraint: Constraint?
///
var showsChevron: Bool {
didSet { updateChevronLayout() }
}
///
private(set) var titleText: String = ""
/// chip
/// - Parameter showsChevron:
init(showsChevron: Bool = false) {
self.showsChevron = showsChevron
super.init(frame: .zero)
setupUI()
updateChevronLayout()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// chip
func apply(title: String) {
titleText = title
titleLabelView.text = title
accessibilityLabel = title
}
private func setupUI() {
backgroundColor = AppColor.pageBackground
layer.cornerRadius = 8
layer.borderWidth = 1
layer.borderColor = AppColor.border.cgColor
titleLabelView.font = .systemFont(ofSize: 11)
titleLabelView.textColor = AppColor.textTertiary
titleLabelView.lineBreakMode = .byTruncatingTail
titleLabelView.isUserInteractionEnabled = false
chevronView.image = UIImage(systemName: "chevron.down")?
.withConfiguration(UIImage.SymbolConfiguration(pointSize: 13, weight: .semibold))
chevronView.tintColor = AppColor.textTertiary
chevronView.contentMode = .scaleAspectFit
chevronView.isUserInteractionEnabled = false
addSubview(titleLabelView)
addSubview(chevronView)
titleLabelView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(10)
make.centerY.equalToSuperview()
titleTrailingToSuperviewConstraint = make.trailing.lessThanOrEqualToSuperview().offset(-10).constraint
titleTrailingToChevronConstraint = make.trailing.lessThanOrEqualTo(chevronView.snp.leading).offset(-4).constraint
}
chevronView.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-10)
make.centerY.equalToSuperview()
make.size.equalTo(16)
}
}
private func updateChevronLayout() {
chevronView.isHidden = !showsChevron
if showsChevron {
titleTrailingToSuperviewConstraint?.deactivate()
titleTrailingToChevronConstraint?.activate()
} else {
titleTrailingToChevronConstraint?.deactivate()
titleTrailingToSuperviewConstraint?.activate()
}
}
}

View File

@ -1187,7 +1187,7 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
button.titleLabel?.adjustsFontSizeToFitWidth = true button.titleLabel?.adjustsFontSizeToFitWidth = true
button.titleLabel?.minimumScaleFactor = 0.82 button.titleLabel?.minimumScaleFactor = 0.82
button.setConfigurationContentInsets( button.setConfigurationContentInsets(
NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10) NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
) )
button.configuration?.imagePlacement = .leading button.configuration?.imagePlacement = .leading
button.configuration?.imagePadding = 4 button.configuration?.imagePadding = 4

View File

@ -328,14 +328,7 @@ final class WildReportRiskMapViewController: BaseViewController {
} }
private func makeRadarHeader(clue: WildReportRadarActiveClue) -> UIView { private func makeRadarHeader(clue: WildReportRadarActiveClue) -> UIView {
let container = UIStackView() let container = UIView()
container.axis = .vertical
container.spacing = 6
let row = UIStackView()
row.axis = .horizontal
row.alignment = .top
row.spacing = 12
let iconWrap = UIView() let iconWrap = UIView()
iconWrap.backgroundColor = headerIconColor(for: clue) iconWrap.backgroundColor = headerIconColor(for: clue)
@ -348,17 +341,15 @@ final class WildReportRiskMapViewController: BaseViewController {
make.center.equalToSuperview() make.center.equalToSuperview()
make.size.equalTo(20) make.size.equalTo(20)
} }
iconWrap.snp.makeConstraints { make in
make.size.equalTo(44)
}
let textStack = UIStackView() let badge = WildReportRiskStatusBadge(text: clue.statusText, color: statusColor(for: clue))
textStack.axis = .vertical badge.setContentHuggingPriority(.required, for: .horizontal)
textStack.spacing = 6 badge.setContentCompressionResistancePriority(.required, for: .horizontal)
var reportMetaText: String?
let title: UILabel
var metaText: String?
if isReportClue(clue) { if isReportClue(clue) {
let title = WildReportUI.label("线索 ID\(clue.id)", font: .systemFont(ofSize: 16, weight: .bold), color: AppColor.textPrimary, lines: 2) title = WildReportUI.label("线索 ID\(clue.id)", font: .systemFont(ofSize: 14, weight: .bold), color: AppColor.textPrimary, lines: 1)
textStack.addArrangedSubview(title)
var metaParts: [String] = [] var metaParts: [String] = []
if let distance = clue.distanceText ?? clue.distance { if let distance = clue.distanceText ?? clue.distance {
metaParts.append("距离:距离您\(distance)") metaParts.append("距离:距离您\(distance)")
@ -366,41 +357,58 @@ final class WildReportRiskMapViewController: BaseViewController {
if let updatedAt = clue.updatedAt { if let updatedAt = clue.updatedAt {
metaParts.append("时间:\(updatedAt)") metaParts.append("时间:\(updatedAt)")
} }
reportMetaText = metaParts.isEmpty ? nil : metaParts.joined(separator: " ") metaText = metaParts.isEmpty ? nil : metaParts.joined(separator: " ")
} else { } else {
let title = WildReportUI.label(clue.displayTitle, font: .systemFont(ofSize: 16, weight: .bold), color: AppColor.textPrimary, lines: 2) title = WildReportUI.label(clue.displayTitle, font: .systemFont(ofSize: 16, weight: .bold), color: AppColor.textPrimary, lines: 2)
textStack.addArrangedSubview(title)
if clue.type == .green { if clue.type == .green {
let metaText = [clue.distance.map { "距蓝点 \($0)" }, clue.direction, clue.updatedAt].compactMap { $0 }.joined(separator: " ") let text = [clue.distance.map { "距蓝点 \($0)" }, clue.direction, clue.updatedAt].compactMap { $0 }.joined(separator: " ")
if !metaText.isEmpty { metaText = text.isEmpty ? nil : text
textStack.addArrangedSubview(WildReportUI.label(metaText, font: .systemFont(ofSize: 12), color: AppColor.textSecondary))
}
} }
} }
let badge = WildReportRiskStatusBadge(text: clue.statusText, color: statusColor(for: clue)) container.addSubview(iconWrap)
row.addArrangedSubview(iconWrap) container.addSubview(badge)
row.addArrangedSubview(textStack) container.addSubview(title)
row.addArrangedSubview(badge)
textStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
badge.setContentHuggingPriority(.required, for: .horizontal)
container.addArrangedSubview(row)
if let reportMetaText { iconWrap.snp.makeConstraints { make in
let metaRow = UIStackView() make.top.leading.equalToSuperview()
metaRow.axis = .horizontal make.size.equalTo(44)
metaRow.spacing = 0
let spacer = UIView()
spacer.snp.makeConstraints { make in
make.width.equalTo(56)
} }
let metaLabel = WildReportUI.label(reportMetaText, font: .systemFont(ofSize: 12), color: AppColor.textSecondary) badge.snp.makeConstraints { make in
metaLabel.textAlignment = .right make.top.trailing.equalToSuperview()
metaLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
metaRow.addArrangedSubview(spacer)
metaRow.addArrangedSubview(metaLabel)
container.addArrangedSubview(metaRow)
} }
title.snp.makeConstraints { make in
make.top.equalToSuperview()
make.leading.equalTo(iconWrap.snp.trailing).offset(12)
make.trailing.lessThanOrEqualTo(badge.snp.leading).offset(-12)
}
if let metaText {
let metaLabel = WildReportUI.label(metaText, font: .systemFont(ofSize: 12), color: AppColor.textSecondary, lines: 0)
container.addSubview(metaLabel)
metaLabel.snp.makeConstraints { make in
make.top.equalTo(title.snp.bottom).offset(6)
make.leading.equalTo(title)
make.trailing.equalToSuperview()
make.bottom.equalToSuperview()
}
iconWrap.snp.makeConstraints { make in
make.bottom.lessThanOrEqualToSuperview()
}
} else {
[iconWrap, title, badge].forEach { view in
view.snp.makeConstraints { make in
make.bottom.lessThanOrEqualToSuperview()
}
}
iconWrap.snp.makeConstraints { make in
make.bottom.equalToSuperview().priority(.high)
}
title.snp.makeConstraints { make in
make.bottom.equalToSuperview().priority(.high)
}
}
return container return container
} }

View File

@ -53,8 +53,9 @@ final class AccountSwitchViewModelTests: XCTestCase {
accountType: V9ScenicUser.accountTypeValue, accountType: V9ScenicUser.accountTypeValue,
businessUserId: 0, businessUserId: 0,
title: "景区A", title: "景区A",
subtitle: "张三", subtitle: "角色A",
phone: "13800138000", phone: "13800138000",
realName: "张三",
avatar: "", avatar: "",
scenicName: "景区A", scenicName: "景区A",
storeId: nil, storeId: nil,

View File

@ -29,6 +29,7 @@ final class AuthModelsTests: XCTestCase {
"phone": "13800138000", "phone": "13800138000",
"scenic_id": 10, "scenic_id": 10,
"scenic_name": "", "scenic_name": "",
"role_name": "",
"is_current": false "is_current": false
} }
], ],
@ -47,6 +48,7 @@ final class AuthModelsTests: XCTestCase {
"scenic_name": "", "scenic_name": "",
"store_id": 20, "store_id": 20,
"store_name": "", "store_name": "",
"role_name": "",
"is_current": false "is_current": false
} }
] ]
@ -59,7 +61,7 @@ final class AuthModelsTests: XCTestCase {
XCTAssertEqual(response.token, "person-temp-token") XCTAssertEqual(response.token, "person-temp-token")
XCTAssertEqual(response.accounts.map(\.businessUserId), [101, 201]) XCTAssertEqual(response.accounts.map(\.businessUserId), [101, 201])
XCTAssertEqual(response.accounts.map(\.subtitle), ["scenic_operator", "store_admin"]) XCTAssertEqual(response.accounts.map(\.subtitle), ["示例景区 · 摄影师", "示例景区 · 店长"])
} }
func testV9AuthResponseDecodesMissingAccountListsAsEmpty() throws { func testV9AuthResponseDecodesMissingAccountListsAsEmpty() throws {
@ -96,13 +98,15 @@ final class AuthModelsTests: XCTestCase {
"phone": "13800138000", "phone": "13800138000",
"scenic_id": 10, "scenic_id": 10,
"scenic_name": "", "scenic_name": "",
"role_name": "",
"is_current": false "is_current": false
} }
""".data(using: .utf8)! """.data(using: .utf8)!
let user = try JSONDecoder().decode(V9ScenicUser.self, from: json) let user = try JSONDecoder().decode(V9ScenicUser.self, from: json)
XCTAssertEqual(user.businessUserId, 101) XCTAssertEqual(user.businessUserId, 101)
XCTAssertEqual(user.toAccountSwitchAccount().subtitle, "scenic_operator") XCTAssertEqual(user.toAccountSwitchAccount().subtitle, "示例景区 · 摄影师")
XCTAssertEqual(user.toAccountSwitchAccount().realName, "张三")
XCTAssertEqual(user.toAccountSwitchAccount().toSetUserRequest(), SetUserRequest(ssUserId: 101)) XCTAssertEqual(user.toAccountSwitchAccount().toSetUserRequest(), SetUserRequest(ssUserId: 101))
} }
@ -122,13 +126,15 @@ final class AuthModelsTests: XCTestCase {
"scenic_name": "", "scenic_name": "",
"store_id": 20, "store_id": 20,
"store_name": "", "store_name": "",
"role_name": "",
"is_current": false "is_current": false
} }
""".data(using: .utf8)! """.data(using: .utf8)!
let user = try JSONDecoder().decode(V9StoreUser.self, from: json) let user = try JSONDecoder().decode(V9StoreUser.self, from: json)
XCTAssertEqual(user.businessUserId, 201) XCTAssertEqual(user.businessUserId, 201)
XCTAssertEqual(user.toAccountSwitchAccount().subtitle, "store_admin") XCTAssertEqual(user.toAccountSwitchAccount().subtitle, "示例景区 · 店长")
XCTAssertEqual(user.toAccountSwitchAccount().realName, "张三")
XCTAssertEqual(user.toAccountSwitchAccount().toSetUserRequest(), SetUserRequest(storeUserId: 201)) XCTAssertEqual(user.toAccountSwitchAccount().toSetUserRequest(), SetUserRequest(storeUserId: 201))
} }
} }

View File

@ -0,0 +1,44 @@
//
// HomeMenuIconFactoryTests.swift
// suixinkanTests
//
import UIKit
import XCTest
@testable import suixinkan
///
final class HomeMenuIconFactoryTests: XCTestCase {
func testAssetIconTakesPrecedence() {
let image = HomeMenuIconFactory.image(named: "home_menu_space")
XCTAssertNotNil(image)
XCTAssertNil(UIImage(systemName: "home_menu_space"))
}
func testSystemSymbolMatchesSharedConfiguration() {
let expected = UIImage(
systemName: "ellipsis",
withConfiguration: HomeMenuIconFactory.symbolConfiguration
)
let actual = HomeMenuIconFactory.image(named: "ellipsis")
XCTAssertNotNil(actual)
XCTAssertEqual(actual?.size, expected?.size)
}
func testSystemSymbolDiffersFromUnconfiguredDefault() {
let configured = HomeMenuIconFactory.image(named: "ellipsis")
let plain = UIImage(systemName: "ellipsis")
XCTAssertNotNil(configured)
XCTAssertNotNil(plain)
XCTAssertNotEqual(configured?.size, plain?.size)
}
func testComplexSystemSymbolResolves() {
XCTAssertNotNil(HomeMenuIconFactory.image(named: "dot.radiowaves.left.and.right"))
XCTAssertNotNil(HomeMenuIconFactory.image(named: "rectangle.stack.badge.plus"))
}
}

View File

@ -0,0 +1,29 @@
//
// WiredTransferSettingChipButtonTests.swift
// suixinkanTests
//
import XCTest
@testable import suixinkan
final class WiredTransferSettingChipButtonTests: XCTestCase {
@MainActor
func testApplyTitleUpdatesAccessibilityLabel() {
let button = WiredTransferSettingChipButton()
button.apply(title: "AI修图")
XCTAssertEqual(button.titleText, "AI修图")
XCTAssertEqual(button.accessibilityLabel, "AI修图")
}
@MainActor
func testShowsChevronDefaultsToFalse() {
let button = WiredTransferSettingChipButton()
XCTAssertFalse(button.showsChevron)
}
@MainActor
func testInitWithShowsChevronTrue() {
let button = WiredTransferSettingChipButton(showsChevron: true)
XCTAssertTrue(button.showsChevron)
}
}