模块化 AppStore 并完善素材管理与个人空间设置。

将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-13 11:01:08 +08:00
parent ceca780ab3
commit 005349f8e6
128 changed files with 2953 additions and 1123 deletions

View File

@ -97,13 +97,13 @@ final class AccountSwitchViewController: BaseViewController, UITableViewDelegate
private var currentAccountId: String? {
let store = AppStore.shared
if store.currentStoreId > 0 {
return "\(V9StoreUser.accountTypeValue)_\(store.currentStoreId)"
if store.session.currentStoreId > 0 {
return "\(V9StoreUser.accountTypeValue)_\(store.session.currentStoreId)"
}
if store.currentScenicId > 0 {
return "\(V9ScenicUser.accountTypeValue)_\(store.userId)"
if store.session.currentScenicId > 0 {
return "\(V9ScenicUser.accountTypeValue)_\(store.session.userId)"
}
return store.userId.isEmpty ? nil : store.userId
return store.session.userId.isEmpty ? nil : store.session.userId
}
private func isCurrentAccount(_ account: AccountSwitchAccount) -> Bool {

View File

@ -156,7 +156,7 @@ final class ProfileEditViewController: BaseViewController {
}
private func saveProfile() async {
let scenicId = AppStore.shared.currentScenicId
let scenicId = AppStore.shared.session.currentScenicId
guard scenicId > 0 else {
showToast("当前景区信息缺失")
return

View File

@ -0,0 +1,448 @@
//
// ProfileSpaceSettingsDialogs.swift
// suixinkan
//
import SnapKit
import UIKit
/// 使 Android
enum ProfileSpaceIcon {
/// 16pt
static var edit: UIImage? {
UIImage(named: "profile_edit")
}
}
/// 使 Android
enum ProfileNavigationStyle {
static func apply(to navigationController: UINavigationController?, showsDivider: Bool) {
guard let navigationBar = navigationController?.navigationBar else { return }
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = .white
appearance.shadowColor = showsDivider ? AppColor.border : .clear
appearance.titleTextAttributes = [
.foregroundColor: UIColor(hex: 0x333333),
.font: UIFont.systemFont(ofSize: 18),
]
navigationBar.standardAppearance = appearance
navigationBar.scrollEdgeAppearance = appearance
navigationBar.compactAppearance = appearance
navigationBar.tintColor = .black
}
static func restore(_ navigationController: UINavigationController?) {
guard let navigationBar = navigationController?.navigationBar else { return }
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = AppColor.cardBackground
appearance.shadowColor = .clear
appearance.titleTextAttributes = [
.foregroundColor: AppColor.textPrimary,
.font: UIFont.app(.title),
]
navigationBar.standardAppearance = appearance
navigationBar.scrollEdgeAppearance = appearance
navigationBar.compactAppearance = appearance
navigationBar.tintColor = AppColor.primary
}
}
/// Android 线
final class ProfileDashedBorderButton: UIButton {
private let dashedLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
dashedLayer.strokeColor = AppColor.primary.cgColor
dashedLayer.fillColor = UIColor.clear.cgColor
dashedLayer.lineWidth = 1
dashedLayer.lineDashPattern = [2, 2]
layer.addSublayer(dashedLayer)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
dashedLayer.frame = bounds
dashedLayer.path = UIBezierPath(roundedRect: bounds.insetBy(dx: 0.5, dy: 0.5), cornerRadius: 4).cgPath
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
bounds.insetBy(dx: -8, dy: -8).contains(point)
}
}
/// Android
class ProfileBaseDialogViewController: UIViewController {
let cardView = UIView()
init() {
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .overFullScreen
modalTransitionStyle = .crossDissolve
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black.withAlphaComponent(0.2)
cardView.backgroundColor = .white
view.addSubview(cardView)
let outsideTap = UITapGestureRecognizer(target: self, action: #selector(outsideTapped(_:)))
outsideTap.cancelsTouchesInView = false
view.addGestureRecognizer(outsideTap)
cardView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.trailing.equalToSuperview().inset(32)
}
}
func makePrimaryButton(title: String, height: CGFloat = 45) -> UIButton {
let button = UIButton(type: .system)
button.setTitle(title, for: .normal)
button.setTitleColor(.white, for: .normal)
button.setTitleColor(.white, for: .disabled)
button.titleLabel?.font = .systemFont(ofSize: 17, weight: .medium)
button.backgroundColor = AppColor.primary
button.layer.cornerRadius = 10
button.snp.makeConstraints { make in make.height.equalTo(height) }
return button
}
@objc private func outsideTapped(_ gesture: UITapGestureRecognizer) {
let location = gesture.location(in: view)
guard !cardView.frame.contains(location) else { return }
dismiss(animated: false)
}
}
/// Android
final class ProfileTimePickerDialogViewController: ProfileBaseDialogViewController {
private let picker = UIDatePicker()
private let confirmButton: UIButton
private let onConfirm: (Date) -> Void
init(initialDate: Date, onConfirm: @escaping (Date) -> Void) {
self.onConfirm = onConfirm
confirmButton = UIButton(type: .system)
super.init()
picker.date = initialDate
}
override func viewDidLoad() {
super.viewDidLoad()
cardView.layer.cornerRadius = 10
picker.datePickerMode = .time
picker.preferredDatePickerStyle = .wheels
picker.locale = Locale(identifier: "zh_CN")
confirmButton.setTitle("确认", for: .normal)
confirmButton.setTitleColor(.white, for: .normal)
confirmButton.titleLabel?.font = .systemFont(ofSize: 17, weight: .medium)
confirmButton.backgroundColor = AppColor.primary
confirmButton.layer.cornerRadius = 10
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
cardView.addSubview(picker)
cardView.addSubview(confirmButton)
picker.snp.makeConstraints { make in
make.top.equalToSuperview().offset(24)
make.leading.trailing.equalToSuperview().inset(15)
make.height.equalTo(180)
}
confirmButton.snp.makeConstraints { make in
make.top.equalTo(picker.snp.bottom).offset(24)
make.leading.trailing.equalToSuperview().inset(15)
make.height.equalTo(45)
make.bottom.equalToSuperview().inset(24)
}
}
@objc private func confirmTapped() {
onConfirm(picker.date)
dismiss(animated: false)
}
}
/// Android
final class ProfileDeviceDialogViewController: ProfileBaseDialogViewController, UITextFieldDelegate {
private let textField = UITextField()
private let confirmButton = UIButton(type: .system)
private let onConfirm: (String) -> Void
init(onConfirm: @escaping (String) -> Void) {
self.onConfirm = onConfirm
super.init()
}
override func viewDidLoad() {
super.viewDidLoad()
cardView.layer.cornerRadius = 10
textField.placeholder = "请输入设备名"
textField.font = .systemFont(ofSize: 14)
textField.layer.borderColor = AppColor.border.cgColor
textField.layer.borderWidth = 1
textField.layer.cornerRadius = 8
textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 1))
textField.leftViewMode = .always
textField.returnKeyType = .done
textField.delegate = self
textField.addTarget(self, action: #selector(textChanged), for: .editingChanged)
confirmButton.setTitle("确认", for: .normal)
confirmButton.setTitleColor(.white, for: .normal)
confirmButton.setTitleColor(.white, for: .disabled)
confirmButton.titleLabel?.font = .systemFont(ofSize: 17, weight: .medium)
confirmButton.layer.cornerRadius = 10
confirmButton.isEnabled = false
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
cardView.addSubview(textField)
cardView.addSubview(confirmButton)
textField.snp.makeConstraints { make in
make.top.equalToSuperview().offset(24)
make.leading.trailing.equalToSuperview().inset(15)
make.height.equalTo(46)
}
confirmButton.snp.makeConstraints { make in
make.top.equalTo(textField.snp.bottom).offset(24)
make.leading.trailing.equalToSuperview().inset(15)
make.height.equalTo(45)
make.bottom.equalToSuperview().inset(24)
}
textChanged()
DispatchQueue.main.async { [weak self] in self?.textField.becomeFirstResponder() }
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
guard confirmButton.isEnabled else { return false }
confirmTapped()
return true
}
@objc private func textChanged() {
let enabled = !(textField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
confirmButton.isEnabled = enabled
confirmButton.backgroundColor = enabled ? AppColor.primary : AppColor.buttonDisabled
}
@objc private func confirmTapped() {
let value = (textField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
guard !value.isEmpty else { return }
onConfirm(value)
dismiss(animated: false)
}
}
/// Android
final class ProfileDeleteDialogViewController: ProfileBaseDialogViewController {
private let dialogTitle: String
private let dialogMessage: String
private let onConfirm: () -> Void
init(title: String, message: String, onConfirm: @escaping () -> Void) {
dialogTitle = title
dialogMessage = message
self.onConfirm = onConfirm
super.init()
}
override func viewDidLoad() {
super.viewDidLoad()
cardView.layer.cornerRadius = 16
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 16
stack.layoutMargins = UIEdgeInsets(top: 24, left: 24, bottom: 24, right: 24)
stack.isLayoutMarginsRelativeArrangement = true
let titleLabel = UILabel()
titleLabel.text = dialogTitle
titleLabel.font = .systemFont(ofSize: 18, weight: .bold)
titleLabel.textColor = UIColor(hex: 0xFF0000)
let messageLabel = UILabel()
messageLabel.font = .systemFont(ofSize: 16)
messageLabel.textColor = UIColor(hex: 0x333333)
messageLabel.numberOfLines = 0
let message = NSMutableAttributedString(
string: dialogMessage,
attributes: [.font: UIFont.systemFont(ofSize: 16), .foregroundColor: UIColor(hex: 0x333333)]
)
let prefix = "您确定要删除"
if dialogMessage.hasPrefix(prefix), dialogTitle != "删除日程" {
let suffixes = ["标签吗?", "拍摄说明吗?", "设备吗?"]
if let suffix = suffixes.first(where: dialogMessage.hasSuffix) {
let nameStart = prefix.count
let nameLength = dialogMessage.count - prefix.count - suffix.count
if nameLength > 0 {
message.addAttribute(.font, value: UIFont.systemFont(ofSize: 16, weight: .bold), range: NSRange(location: nameStart, length: nameLength))
}
}
}
messageLabel.attributedText = message
let buttons = UIStackView()
buttons.axis = .horizontal
buttons.spacing = 12
buttons.distribution = .fillEqually
let cancel = UIButton(type: .system)
cancel.setTitle("取消", for: .normal)
cancel.setTitleColor(UIColor(hex: 0xFF0000), for: .normal)
cancel.backgroundColor = UIColor(hex: 0xFFE5E5)
cancel.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
cancel.layer.cornerRadius = 8
cancel.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
let confirm = UIButton(type: .system)
confirm.setTitle("确认", for: .normal)
confirm.setTitleColor(.white, for: .normal)
confirm.backgroundColor = UIColor(hex: 0xFF0000)
confirm.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
confirm.layer.cornerRadius = 8
confirm.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
[cancel, confirm].forEach { button in
button.snp.makeConstraints { make in make.height.equalTo(44) }
buttons.addArrangedSubview(button)
}
stack.addArrangedSubview(titleLabel)
stack.addArrangedSubview(messageLabel)
stack.setCustomSpacing(24, after: messageLabel)
stack.addArrangedSubview(buttons)
cardView.addSubview(stack)
stack.snp.makeConstraints { make in make.edges.equalToSuperview() }
}
@objc private func cancelTapped() { dismiss(animated: false) }
@objc private func confirmTapped() {
dismiss(animated: false) { [onConfirm] in onConfirm() }
}
}
/// Android 480×480 JPEG
final class ProfileAvatarCropViewController: UIViewController, UIScrollViewDelegate {
private let sourceImage: UIImage
private let onConfirm: (Data) -> Void
private let scrollView = UIScrollView()
private let imageView = UIImageView()
private let cancelButton = UIButton(type: .system)
private let confirmButton = UIButton(type: .system)
private var didConfigureZoom = false
init(image: UIImage, onConfirm: @escaping (Data) -> Void) {
sourceImage = Self.normalized(image)
self.onConfirm = onConfirm
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
scrollView.delegate = self
scrollView.clipsToBounds = true
scrollView.layer.borderColor = UIColor.white.cgColor
scrollView.layer.borderWidth = 1
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
imageView.image = sourceImage
imageView.contentMode = .scaleAspectFit
imageView.frame = CGRect(origin: .zero, size: sourceImage.size)
scrollView.addSubview(imageView)
scrollView.contentSize = sourceImage.size
cancelButton.setTitle("取消", for: .normal)
confirmButton.setTitle("确认", for: .normal)
[cancelButton, confirmButton].forEach {
$0.setTitleColor(.white, for: .normal)
$0.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
}
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
view.addSubview(scrollView)
view.addSubview(cancelButton)
view.addSubview(confirmButton)
scrollView.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.centerY.equalToSuperview()
make.height.equalTo(scrollView.snp.width)
}
cancelButton.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(20)
make.top.equalTo(view.safeAreaLayoutGuide).offset(10)
make.width.height.greaterThanOrEqualTo(44)
}
confirmButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(20)
make.top.equalTo(view.safeAreaLayoutGuide).offset(10)
make.width.height.greaterThanOrEqualTo(44)
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard !didConfigureZoom, scrollView.bounds.width > 0 else { return }
didConfigureZoom = true
let minimumZoom = max(
scrollView.bounds.width / sourceImage.size.width,
scrollView.bounds.height / sourceImage.size.height
)
scrollView.minimumZoomScale = minimumZoom
scrollView.maximumZoomScale = max(minimumZoom * 6, 6)
scrollView.zoomScale = minimumZoom
let contentWidth = sourceImage.size.width * minimumZoom
let contentHeight = sourceImage.size.height * minimumZoom
scrollView.contentOffset = CGPoint(
x: max(0, (contentWidth - scrollView.bounds.width) / 2),
y: max(0, (contentHeight - scrollView.bounds.height) / 2)
)
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? { imageView }
@objc private func cancelTapped() { dismiss(animated: true) }
@objc private func confirmTapped() {
guard let data = renderedCropData() else { return }
dismiss(animated: true) { [onConfirm] in onConfirm(data) }
}
func renderedCropData() -> Data? {
let cropRectInImageView = scrollView.convert(scrollView.bounds, to: imageView)
let scaleX = sourceImage.size.width / imageView.bounds.width
let scaleY = sourceImage.size.height / imageView.bounds.height
var cropRect = CGRect(
x: cropRectInImageView.minX * scaleX,
y: cropRectInImageView.minY * scaleY,
width: cropRectInImageView.width * scaleX,
height: cropRectInImageView.height * scaleY
).integral
cropRect = cropRect.intersection(CGRect(origin: .zero, size: sourceImage.size))
guard !cropRect.isEmpty, let cgImage = sourceImage.cgImage?.cropping(to: cropRect) else { return nil }
let cropped = UIImage(cgImage: cgImage)
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 480, height: 480))
return renderer.image { _ in
cropped.draw(in: CGRect(x: 0, y: 0, width: 480, height: 480))
}.jpegData(compressionQuality: 0.9)
}
private static func normalized(_ image: UIImage) -> UIImage {
guard image.imageOrientation != .up else { return image }
let renderer = UIGraphicsImageRenderer(size: image.size)
return renderer.image { _ in image.draw(in: CGRect(origin: .zero, size: image.size)) }
}
}

View File

@ -13,7 +13,6 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
private let viewModel: ProfileSpaceSettingsViewModel
private let profileAPI: ProfileAPI
private let ossUploadService: OSSUploadService
private let scrollView = UIScrollView()
private let contentStack = UIStackView()
@ -21,17 +20,22 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
private let saveButton = AppButton(title: "保存")
private let avatarImageView = UIImageView()
private let avatarChangeOverlay = UIView()
private let avatarChangeLabel = UILabel()
private let realNameLabel = UILabel()
private let nicknameField = UITextField()
private let editProfileButton = UIButton(type: .system)
private let introductionTextView = UITextView()
private let introductionPlaceholderLabel = UILabel()
private let certificationWrap = ProfileTagWrapView()
private let certificationExpandButton = UIButton(type: .system)
private let attrWrap = ProfileTagWrapView()
private let shootWrap = ProfileTagWrapView()
private let attrField = UITextField()
private let shootField = UITextField()
private let attrAddButton = UIButton(type: .system)
private let shootAddButton = UIButton(type: .system)
private let businessStartButton = ProfileTimeButton()
private let businessEndButton = ProfileTimeButton()
@ -39,7 +43,7 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
private let holidayEndButton = ProfileTimeButton()
private let deviceWrap = ProfileTagWrapView()
private let addDeviceButton = UIButton(type: .system)
private let addDeviceButton = ProfileDashedBorderButton(type: .system)
private var statusButtons: [UIButton] = []
private let dateStack = UIStackView()
@ -49,12 +53,10 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
init(
viewModel: ProfileSpaceSettingsViewModel = ProfileSpaceSettingsViewModel(),
profileAPI: ProfileAPI = NetworkServices.shared.profileAPI,
ossUploadService: OSSUploadService = NetworkServices.shared.ossUploadService
profileAPI: ProfileAPI = NetworkServices.shared.profileAPI
) {
self.viewModel = viewModel
self.profileAPI = profileAPI
self.ossUploadService = ossUploadService
super.init(nibName: nil, bundle: nil)
}
@ -65,6 +67,17 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
override func setupNavigationBar() {
title = "个人空间配置"
navigationItem.backButtonDisplayMode = .minimal
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
ProfileNavigationStyle.apply(to: navigationController, showsDivider: false)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
ProfileNavigationStyle.restore(navigationController)
}
override func setupUI() {
@ -82,6 +95,7 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
bottomBar.backgroundColor = .white
saveButton.isEnabled = false
saveButton.snp.updateConstraints { make in make.height.equalTo(48) }
configureProfileCard()
configureLabelCard()
@ -96,7 +110,9 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
make.leading.trailing.bottom.equalToSuperview()
}
saveButton.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 16, left: 15, bottom: 16, right: 15))
make.top.equalToSuperview().offset(16)
make.leading.trailing.equalToSuperview().inset(15)
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(16)
}
scrollView.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
@ -117,8 +133,11 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
}
saveButton.addTarget(self, action: #selector(saveTapped), for: .touchUpInside)
editProfileButton.addTarget(self, action: #selector(editProfileTapped), for: .touchUpInside)
certificationExpandButton.addTarget(self, action: #selector(certificationExpandTapped), for: .touchUpInside)
addDeviceButton.addTarget(self, action: #selector(addDeviceTapped), for: .touchUpInside)
addScheduleButton.addTarget(self, action: #selector(addScheduleTapped), for: .touchUpInside)
attrField.addTarget(self, action: #selector(labelInputChanged), for: .editingChanged)
shootField.addTarget(self, action: #selector(labelInputChanged), for: .editingChanged)
[businessStartButton, businessEndButton, holidayStartButton, holidayEndButton].forEach {
$0.addTarget(self, action: #selector(timeButtonTapped(_:)), for: .touchUpInside)
}
@ -146,17 +165,23 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
nicknameField.isEnabled = viewModel.isEditingProfile
introductionTextView.isEditable = viewModel.isEditingProfile
introductionTextView.text = viewModel.introduction.isEmpty && !viewModel.isEditingProfile ? "暂无简介" : viewModel.introduction
introductionTextView.textColor = viewModel.introduction.isEmpty && !viewModel.isEditingProfile ? UIColor(hex: 0xB6BECA) : UIColor(hex: 0x565656)
introductionTextView.textColor = viewModel.introduction.isEmpty && !viewModel.isEditingProfile ? UIColor(hex: 0xB6BECA) : UIColor(hex: 0x4B5563)
updateIntroductionPlaceholder()
editProfileButton.setImage(UIImage(systemName: viewModel.isEditingProfile ? "checkmark" : "square.and.pencil"), for: .normal)
certificationWrap.setItems(viewModel.scenicCertification, style: .certification, removable: false)
editProfileButton.setImage(ProfileSpaceIcon.edit, for: .normal)
avatarChangeOverlay.isHidden = !viewModel.isEditingProfile
let visibleCertifications = viewModel.isCertificationExpanded
? viewModel.scenicCertification
: Array(viewModel.scenicCertification.prefix(3))
certificationWrap.setItems(visibleCertifications, style: .certification, removable: false)
certificationExpandButton.isHidden = viewModel.scenicCertification.count <= 3
certificationExpandButton.setTitle(viewModel.isCertificationExpanded ? "收起" : "展开", for: .normal)
attrWrap.setItems(viewModel.attrLabels, style: .label, removable: true) { [weak self] label in
self?.confirmDelete(title: "删除确认", message: "确认删除标签“\(label)”吗?") {
self?.confirmDelete(title: "删除标签", message: "您确定要删除\(label)标签吗?") {
self?.viewModel.removeAttrLabel(label)
}
}
shootWrap.setItems(viewModel.shootLabels, style: .label, removable: true) { [weak self] label in
self?.confirmDelete(title: "删除确认", message: "确认删除拍摄说明“\(label)”吗?") {
self?.confirmDelete(title: "删除拍摄说明", message: "您确定要删除\(label)拍摄说明吗?") {
self?.viewModel.removeShootLabel(label)
}
}
@ -165,12 +190,15 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
holidayStartButton.setTitle(viewModel.holidayStartTime ?? "00:00", for: .normal)
holidayEndButton.setTitle(viewModel.holidayEndTime ?? "00:00", for: .normal)
deviceWrap.setItems(viewModel.cameraDevices, style: .device, removable: true) { [weak self] device in
self?.confirmDelete(title: "删除确认", message: "确认删除设备“\(device)”吗?") {
self?.confirmDelete(title: "删除设备", message: "您确定要删除\(device)设备吗?") {
self?.viewModel.removeDevice(device)
}
}
addDeviceButton.isHidden = viewModel.cameraDevices.count > 10
deviceWrap.invalidateIntrinsicContentSize()
deviceWrap.setNeedsLayout()
updateStatusButtons()
updateLabelAddButtons()
rebuildDateButtons()
rebuildSchedules()
}
@ -184,20 +212,34 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
avatarImageView.layer.cornerRadius = 58
avatarImageView.clipsToBounds = true
avatarImageView.backgroundColor = AppColor.inputBackground
avatarImageView.backgroundColor = AppColor.primaryLight
avatarImageView.contentMode = .scaleAspectFill
avatarImageView.image = UIImage(systemName: "person.crop.circle.fill")
avatarImageView.image = UIImage(named: "profile_avatar_default")
avatarImageView.tintColor = UIColor(hex: 0xB6BECA)
avatarImageView.accessibilityLabel = "头像"
avatarImageView.snp.makeConstraints { make in
make.width.height.equalTo(116)
}
avatarImageView.addSubview(avatarChangeOverlay)
avatarChangeOverlay.backgroundColor = UIColor.black.withAlphaComponent(0.35)
avatarChangeOverlay.isHidden = true
avatarChangeOverlay.addSubview(avatarChangeLabel)
avatarChangeLabel.text = "点击更换"
avatarChangeLabel.textColor = .white
avatarChangeLabel.font = .systemFont(ofSize: 12, weight: .medium)
avatarChangeOverlay.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
avatarChangeLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
}
let textStack = UIStackView()
textStack.axis = .vertical
textStack.spacing = 8
textStack.spacing = 0
realNameLabel.font = .systemFont(ofSize: 14)
realNameLabel.textColor = UIColor(hex: 0x565656)
realNameLabel.textColor = UIColor(hex: 0x4B5563)
let nicknameRow = UIStackView()
nicknameRow.axis = .horizontal
@ -211,8 +253,9 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
nicknameField.delegate = self
nicknameField.addTarget(self, action: #selector(nicknameChanged), for: .editingChanged)
editProfileButton.tintColor = AppColor.primary
editProfileButton.accessibilityLabel = "编辑个人信息"
editProfileButton.snp.makeConstraints { make in
make.width.height.equalTo(28)
make.width.height.equalTo(44)
}
nicknameRow.addArrangedSubview(nicknameField)
nicknameRow.addArrangedSubview(editProfileButton)
@ -236,8 +279,18 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
textStack.addArrangedSubview(realNameLabel)
textStack.addArrangedSubview(nicknameRow)
textStack.setCustomSpacing(8, after: nicknameRow)
textStack.addArrangedSubview(introductionTextView)
textStack.setCustomSpacing(4, after: introductionTextView)
textStack.addArrangedSubview(certificationWrap)
certificationExpandButton.setTitleColor(AppColor.primary, for: .normal)
certificationExpandButton.titleLabel?.font = .systemFont(ofSize: 14)
certificationExpandButton.contentHorizontalAlignment = .left
certificationExpandButton.isHidden = true
certificationExpandButton.snp.makeConstraints { make in
make.height.greaterThanOrEqualTo(20)
}
textStack.addArrangedSubview(certificationExpandButton)
row.addArrangedSubview(avatarImageView)
row.addArrangedSubview(textStack)
@ -247,11 +300,30 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
private func configureLabelCard() {
let card = makeCard()
card.addArrangedSubview(makeSectionTitle("标签"))
card.spacing = 0
let title = makeSectionTitle("标签")
card.addArrangedSubview(title)
card.setCustomSpacing(12, after: title)
card.addArrangedSubview(attrWrap)
card.addArrangedSubview(makeAddRow(field: attrField, placeholder: "添加标签", action: #selector(addAttrTapped)))
card.setCustomSpacing(8, after: attrWrap)
let attrRow = makeAddRow(
field: attrField,
button: attrAddButton,
placeholder: "添加标签",
action: #selector(addAttrTapped)
)
card.addArrangedSubview(attrRow)
card.setCustomSpacing(20, after: attrRow)
card.addArrangedSubview(shootWrap)
card.addArrangedSubview(makeAddRow(field: shootField, placeholder: "添加拍摄说明", action: #selector(addShootTapped)))
card.setCustomSpacing(12, after: shootWrap)
let shootRow = makeAddRow(
field: shootField,
button: shootAddButton,
placeholder: "添加拍摄说明",
action: #selector(addShootTapped)
)
card.addArrangedSubview(shootRow)
card.setCustomSpacing(8, after: shootRow)
let hint = UILabel()
hint.text = "每个标签最多20个字符,最多8个标签"
hint.font = .systemFont(ofSize: 12)
@ -262,8 +334,11 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
private func configureBusinessHoursCard() {
let card = makeCard()
card.addArrangedSubview(makeSectionTitle("营业时间"))
card.addArrangedSubview(makeTimeRow(title: "工作日营业时间", start: businessStartButton, end: businessEndButton))
let title = makeSectionTitle("营业时间")
let workdayRow = makeTimeRow(title: "工作日营业时间", start: businessStartButton, end: businessEndButton)
card.addArrangedSubview(title)
card.addArrangedSubview(workdayRow)
card.setCustomSpacing(20, after: workdayRow)
card.addArrangedSubview(makeTimeRow(title: "法定节假日营业时间", start: holidayStartButton, end: holidayEndButton))
contentStack.addArrangedSubview(card)
}
@ -272,16 +347,14 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
let card = makeCard()
card.addArrangedSubview(makeSectionTitle("相机设备"))
card.addArrangedSubview(deviceWrap)
addDeviceButton.setTitle("+ 添加设备", for: .normal)
addDeviceButton.setTitle(" 添加设备", for: .normal)
addDeviceButton.setTitleColor(AppColor.primary, for: .normal)
addDeviceButton.titleLabel?.font = .systemFont(ofSize: 14)
addDeviceButton.layer.borderColor = AppColor.primary.cgColor
addDeviceButton.layer.borderWidth = 1
addDeviceButton.layer.cornerRadius = 4
addDeviceButton.snp.makeConstraints { make in
make.height.equalTo(32)
}
card.addArrangedSubview(addDeviceButton)
addDeviceButton.contentEdgeInsets = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)
addDeviceButton.contentHorizontalAlignment = .left
addDeviceButton.snp.makeConstraints { make in make.height.equalTo(28) }
deviceWrap.setTrailingView(addDeviceButton)
contentStack.addArrangedSubview(card)
}
@ -335,6 +408,7 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
header.addArrangedSubview(UIView())
header.addArrangedSubview(addScheduleButton)
card.addArrangedSubview(header)
card.setCustomSpacing(8, after: header)
scheduleStack.axis = .vertical
scheduleStack.spacing = 12
@ -358,11 +432,18 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
let label = UILabel()
label.text = title
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = UIColor(hex: 0x252525)
label.textColor = ["相机设备", "接单状态", "日程安排"].contains(title)
? UIColor(hex: 0x4B5563)
: UIColor(hex: 0x252525)
return label
}
private func makeAddRow(field: UITextField, placeholder: String, action: Selector) -> UIView {
private func makeAddRow(
field: UITextField,
button: UIButton,
placeholder: String,
action: Selector
) -> UIView {
let row = UIStackView()
row.axis = .horizontal
row.spacing = 8
@ -379,13 +460,14 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
field.snp.makeConstraints { make in
make.height.equalTo(36)
}
let button = UIButton(type: .system)
button.setTitle("添加标签", for: .normal)
button.setTitleColor(.white, for: .normal)
button.setTitleColor(.white, for: .disabled)
button.backgroundColor = AppColor.primary
button.titleLabel?.font = .systemFont(ofSize: 14)
button.layer.cornerRadius = 4
button.addTarget(self, action: action, for: .touchUpInside)
button.isEnabled = false
button.snp.makeConstraints { make in
make.width.equalTo(80)
make.height.equalTo(36)
@ -403,12 +485,12 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.font = .systemFont(ofSize: 14)
titleLabel.textColor = UIColor(hex: 0x565656)
titleLabel.textColor = UIColor(hex: 0x4B5563)
titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
let toLabel = UILabel()
toLabel.text = ""
toLabel.font = .systemFont(ofSize: 14)
toLabel.textColor = UIColor(hex: 0x565656)
toLabel.textColor = UIColor(hex: 0x4B5563)
let spacer = UIView()
row.addArrangedSubview(titleLabel)
row.addArrangedSubview(spacer)
@ -417,7 +499,7 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
row.addArrangedSubview(end)
[start, end].forEach { button in
button.snp.makeConstraints { make in
make.width.equalTo(70)
make.width.equalTo(60)
make.height.equalTo(36)
}
}
@ -428,8 +510,8 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
let trimmed = urlString?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !trimmed.isEmpty else {
avatarImageView.kf.cancelDownloadTask()
avatarImageView.backgroundColor = AppColor.inputBackground
avatarImageView.image = UIImage(systemName: "person.crop.circle.fill")
avatarImageView.backgroundColor = AppColor.primaryLight
avatarImageView.image = UIImage(named: "profile_avatar_default")
avatarImageView.tintColor = UIColor(hex: 0xB6BECA)
avatarImageView.contentMode = .scaleAspectFit
return
@ -440,6 +522,26 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
private func updateIntroductionPlaceholder() {
introductionPlaceholderLabel.isHidden = !viewModel.isEditingProfile || !introductionTextView.text.isEmpty
introductionTextView.layer.borderWidth = viewModel.isEditingProfile ? 1 : 0
introductionTextView.layer.borderColor = UIColor(hex: 0xEEEEEE).cgColor
introductionTextView.layer.cornerRadius = 4
introductionTextView.textContainerInset = viewModel.isEditingProfile
? UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
: .zero
introductionPlaceholderLabel.snp.remakeConstraints { make in
make.top.equalToSuperview().offset(viewModel.isEditingProfile ? 8 : 0)
make.leading.equalToSuperview().offset(viewModel.isEditingProfile ? 8 : 0)
make.trailing.equalToSuperview().inset(viewModel.isEditingProfile ? 8 : 0)
}
}
private func updateLabelAddButtons() {
let pairs = [(attrField, attrAddButton), (shootField, shootAddButton)]
for (field, button) in pairs {
let enabled = !(field.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
button.isEnabled = enabled
button.backgroundColor = enabled ? AppColor.primary : AppColor.buttonDisabled
}
}
private func updateStatusButtons() {
@ -447,7 +549,8 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
let selected = button.tag == viewModel.acceptOrderStatus
button.backgroundColor = selected ? AppColor.primary : .white
button.layer.borderColor = (selected ? AppColor.primary : UIColor(hex: 0xB6BECA)).cgColor
button.setTitleColor(selected ? .white : UIColor(hex: 0x565656), for: .normal)
button.setTitleColor(selected ? .white : UIColor(hex: 0x4B5563), for: .normal)
button.accessibilityTraits = selected ? [.button, .selected] : .button
}
}
@ -485,7 +588,7 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
for schedule in schedules {
let item = ProfileScheduleItemView(schedule: schedule)
item.onDelete = { [weak self] in
self?.confirmDelete(title: "删除确认", message: "确认删除该日程吗") {
self?.confirmDelete(title: "删除日程", message: "您确定要删除该日程吗?") {
guard let self else { return }
Task { await self.viewModel.deleteSchedule(id: schedule.id, api: self.profileAPI) }
}
@ -498,32 +601,20 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
}
private func confirmDelete(title: String, message: String, onConfirm: @escaping () -> Void) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
alert.addAction(UIAlertAction(title: "确定", style: .destructive) { _ in onConfirm() })
present(alert, animated: true)
let dialog = ProfileDeleteDialogViewController(title: title, message: message, onConfirm: onConfirm)
present(dialog, animated: false)
}
private func showTimePicker(initialTime: String?, onConfirm: @escaping (String) -> Void) {
let alert = UIAlertController(title: "选择时间", message: "\n\n\n\n\n\n\n", preferredStyle: .actionSheet)
let picker = UIDatePicker()
picker.datePickerMode = .time
picker.preferredDatePickerStyle = .wheels
picker.locale = Locale(identifier: "zh_CN")
if let initialTime {
picker.date = date(fromTime: initialTime)
}
alert.view.addSubview(picker)
picker.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(16)
make.top.equalToSuperview().offset(42)
make.height.equalTo(160)
}
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
alert.addAction(UIAlertAction(title: "确定", style: .default) { _ in
onConfirm(self.timeString(from: picker.date))
})
present(alert, animated: true)
let dialog = ProfileTimePickerDialogViewController(
initialDate: date(fromTime: "00:00"),
onConfirm: { [weak self] date in
guard let self else { return }
onConfirm(timeString(from: date))
}
)
present(dialog, animated: false)
_ = initialTime
}
private func date(fromTime value: String) -> Date {
@ -564,11 +655,21 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
@objc private func addAttrTapped() {
viewModel.addAttrLabel(attrField.text ?? "")
attrField.text = ""
updateLabelAddButtons()
}
@objc private func addShootTapped() {
viewModel.addShootLabel(shootField.text ?? "")
shootField.text = ""
updateLabelAddButtons()
}
@objc private func labelInputChanged() {
updateLabelAddButtons()
}
@objc private func certificationExpandTapped() {
viewModel.toggleCertificationExpanded()
}
@objc private func timeButtonTapped(_ sender: ProfileTimeButton) {
@ -584,17 +685,10 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
}
@objc private func addDeviceTapped() {
let alert = UIAlertController(title: "添加设备", message: nil, preferredStyle: .alert)
alert.addTextField { field in
field.placeholder = "请输入设备名称"
field.returnKeyType = .done
field.delegate = self
let dialog = ProfileDeviceDialogViewController { [weak self] value in
self?.viewModel.addDevice(value)
}
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
alert.addAction(UIAlertAction(title: "确定", style: .default) { [weak self] _ in
self?.viewModel.addDevice(alert.textFields?.first?.text ?? "")
})
present(alert, animated: true)
present(dialog, animated: false)
}
@objc private func statusTapped(_ sender: UIButton) {
@ -632,6 +726,17 @@ extension ProfileSpaceSettingsViewController: UITextFieldDelegate, UITextViewDel
return true
}
func textField(
_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String
) -> Bool {
guard textField === attrField || textField === shootField else { return true }
let current = textField.text ?? ""
guard let swiftRange = Range(range, in: current) else { return false }
return current.replacingCharacters(in: swiftRange, with: string).count <= 20
}
func textViewDidChange(_ textView: UITextView) {
viewModel.updateIntroduction(textView.text)
updateIntroductionPlaceholder()
@ -654,17 +759,19 @@ extension ProfileSpaceSettingsViewController: PHPickerViewControllerDelegate {
guard let provider = results.first?.itemProvider else { return }
Task {
do {
let data = try await ProfileAvatarPickerLoader.loadImageData(from: provider)
let scenicId = AppStore.shared.currentScenicId
let fileURL = try await ossUploadService.uploadUserAvatar(
data: data,
fileName: "avatar_\(UUID().uuidString).jpg",
scenicId: scenicId
) { _ in }
try await profileAPI.updateUserInfo(nickname: nil, password: nil, avatar: fileURL)
let image = try await ProfileAvatarPickerLoader.loadImage(from: provider)
await MainActor.run {
viewModel.updateAvatarURL(fileURL)
showToast("修改成功")
let crop = ProfileAvatarCropViewController(image: image) { [weak self] data in
guard let self else { return }
Task {
await self.viewModel.uploadAvatar(
data: data,
fileName: "avatar_\(UUID().uuidString).jpg",
api: self.profileAPI
)
}
}
self.present(crop, animated: true)
}
} catch {
await MainActor.run { showToast(error.localizedDescription) }
@ -690,6 +797,7 @@ final class ProfileAddScheduleViewController: BaseViewController {
private let remarkTextView = UITextView()
private let remarkPlaceholderLabel = UILabel()
private let countLabel = UILabel()
private var selectedOrderNumber: String?
init(selectedDate: Date, viewModel: ProfileSpaceSettingsViewModel, profileAPI: ProfileAPI) {
self.selectedDate = selectedDate
@ -707,11 +815,24 @@ final class ProfileAddScheduleViewController: BaseViewController {
title = "添加日程"
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
ProfileNavigationStyle.apply(to: navigationController, showsDivider: true)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
ProfileNavigationStyle.restore(navigationController)
}
override func setupUI() {
view.addSubview(contentStack)
view.addSubview(bottomBar)
bottomBar.addSubview(saveButton)
bottomBar.backgroundColor = .white
view.backgroundColor = AppColor.pageBackground
saveButton.isEnabled = false
saveButton.snp.updateConstraints { make in make.height.equalTo(48) }
contentStack.axis = .vertical
contentStack.spacing = 20
@ -736,7 +857,9 @@ final class ProfileAddScheduleViewController: BaseViewController {
make.leading.trailing.bottom.equalToSuperview()
}
saveButton.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 16, left: 15, bottom: 16, right: 15))
make.top.equalToSuperview().offset(16)
make.leading.trailing.equalToSuperview().inset(15)
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(16)
}
contentStack.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(16)
@ -759,6 +882,7 @@ final class ProfileAddScheduleViewController: BaseViewController {
endButton.addTarget(self, action: #selector(endTapped), for: .touchUpInside)
orderButton.addTarget(self, action: #selector(orderTapped), for: .touchUpInside)
remarkTextView.delegate = self
nameField.addTarget(self, action: #selector(formValueChanged), for: .editingChanged)
}
private func makeTitle(_ title: String) -> UILabel {
@ -789,15 +913,15 @@ final class ProfileAddScheduleViewController: BaseViewController {
let title = UILabel()
title.text = "时间"
title.font = .systemFont(ofSize: 14)
title.textColor = UIColor(hex: 0x565656)
title.textColor = UIColor(hex: 0x4B5563)
let toLabel = UILabel()
toLabel.text = ""
toLabel.font = .systemFont(ofSize: 14)
toLabel.textColor = UIColor(hex: 0x565656)
toLabel.textColor = UIColor(hex: 0x4B5563)
[startButton, endButton].forEach { button in
button.setTitle("请选择", for: .normal)
button.snp.makeConstraints { make in
make.width.equalTo(78)
make.width.equalTo(60)
make.height.equalTo(36)
}
}
@ -818,17 +942,25 @@ final class ProfileAddScheduleViewController: BaseViewController {
orderButton.layer.borderWidth = 1
orderButton.layer.cornerRadius = 8
orderButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
var configuration = UIButton.Configuration.plain()
configuration.title = "关联订单 (可选)"
configuration.image = UIImage(systemName: "chevron.down")
configuration.imagePlacement = .trailing
configuration.imagePadding = 8
configuration.baseForegroundColor = .black
configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
orderButton.configuration = configuration
}
private func configureRemarkView() {
remarkTextView.font = .systemFont(ofSize: 14)
remarkTextView.textColor = UIColor(hex: 0x565656)
remarkTextView.textColor = UIColor(hex: 0x4B5563)
remarkTextView.layer.borderColor = UIColor(hex: 0xE5E7EB).cgColor
remarkTextView.layer.borderWidth = 1
remarkTextView.layer.cornerRadius = 8
remarkTextView.textContainerInset = UIEdgeInsets(top: 10, left: 12, bottom: 10, right: 12)
remarkTextView.textContainer.lineFragmentPadding = 0
remarkPlaceholderLabel.text = "请输入备注信息"
remarkPlaceholderLabel.text = "请输入日程内容"
remarkPlaceholderLabel.font = remarkTextView.font
remarkPlaceholderLabel.textColor = UIColor(hex: 0xB6BECA)
remarkTextView.addSubview(remarkPlaceholderLabel)
@ -844,25 +976,14 @@ final class ProfileAddScheduleViewController: BaseViewController {
}
private func showTimePicker(initialTime: String?, onConfirm: @escaping (String) -> Void) {
let alert = UIAlertController(title: "选择时间", message: "\n\n\n\n\n\n\n", preferredStyle: .actionSheet)
let picker = UIDatePicker()
picker.datePickerMode = .time
picker.preferredDatePickerStyle = .wheels
picker.locale = Locale(identifier: "zh_CN")
alert.view.addSubview(picker)
picker.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(16)
make.top.equalToSuperview().offset(42)
make.height.equalTo(160)
}
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
alert.addAction(UIAlertAction(title: "确定", style: .default) { _ in
let date = Self.date(from: "00:00")
let dialog = ProfileTimePickerDialogViewController(initialDate: date) { selectedDate in
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "HH:mm"
onConfirm(formatter.string(from: picker.date))
})
present(alert, animated: true)
onConfirm(formatter.string(from: selectedDate))
}
present(dialog, animated: false)
_ = initialTime
}
@ -873,6 +994,7 @@ final class ProfileAddScheduleViewController: BaseViewController {
self?.endButton.setTitle("请选择", for: .normal)
}
self?.startButton.setTitle(value, for: .normal)
self?.updateSaveEnabled()
}
}
@ -883,11 +1005,17 @@ final class ProfileAddScheduleViewController: BaseViewController {
return
}
self?.endButton.setTitle(value, for: .normal)
self?.updateSaveEnabled()
}
}
@objc private func orderTapped() {
showToast("关联订单 (可选)")
let controller = TaskOrderSelectViewController(initialOrderNumber: selectedOrderNumber)
controller.onConfirmed = { [weak self] orderNumber in
self?.selectedOrderNumber = orderNumber
self?.applySelectedOrder()
}
navigationController?.pushViewController(controller, animated: true)
}
@objc private func saveTapped() {
@ -901,7 +1029,7 @@ final class ProfileAddScheduleViewController: BaseViewController {
startTime: start,
endTime: end,
scheduleDate: selectedDate,
orderNumber: nil,
orderNumber: selectedOrderNumber,
api: profileAPI
)
if success {
@ -909,6 +1037,34 @@ final class ProfileAddScheduleViewController: BaseViewController {
}
}
}
@objc private func formValueChanged() {
updateSaveEnabled()
}
private func updateSaveEnabled() {
let name = (nameField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let start = startButton.title(for: .normal)
let end = endButton.title(for: .normal)
saveButton.isEnabled = !name.isEmpty
&& start != nil && start != "请选择"
&& end != nil && end != "请选择"
&& ProfileSpaceSettingsViewModel.isStart(start ?? "", before: end ?? "")
}
private func applySelectedOrder() {
var configuration = orderButton.configuration ?? .plain()
configuration.title = selectedOrderNumber ?? "关联订单 (可选)"
configuration.baseForegroundColor = .black
orderButton.configuration = configuration
}
private static func date(from time: String?) -> Date {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "HH:mm"
return formatter.date(from: time ?? "00:00") ?? Date()
}
}
extension ProfileAddScheduleViewController: UITextFieldDelegate, UITextViewDelegate {
@ -941,35 +1097,69 @@ private final class ProfileTimeButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
titleLabel?.font = .systemFont(ofSize: 14)
setTitleColor(AppColor.textPrimary, for: .normal)
backgroundColor = .white
layer.cornerRadius = 8
layer.borderColor = AppColor.border.cgColor
layer.borderWidth = 1
titleLabel?.lineBreakMode = .byClipping
setTitleColor(UIColor(hex: 0x4B5563), for: .normal)
backgroundColor = AppColor.inputBackground
layer.cornerRadius = 4
contentEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
contentHorizontalAlignment = .center
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
bounds.insetBy(dx: -4, dy: -4).contains(point)
}
}
///
private final class ProfileDateButton: UIButton {
var date: Date?
func configure(weekday: String, day: String, selected: Bool) {
let title = "\(weekday)\n\(day)"
setTitle(title, for: .normal)
titleLabel?.numberOfLines = 2
titleLabel?.textAlignment = .center
titleLabel?.font = .systemFont(ofSize: 14)
setTitleColor(selected ? .white : UIColor(hex: 0x565656), for: .normal)
backgroundColor = selected ? AppColor.primary : AppColor.inputBackground
layer.cornerRadius = 4
snp.makeConstraints { make in
make.height.equalTo(58)
private let weekdayLabel = UILabel()
private let dayContainer = UIView()
private let dayLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
weekdayLabel.font = .systemFont(ofSize: 14)
weekdayLabel.textAlignment = .center
weekdayLabel.textColor = UIColor(hex: 0x4B5563)
dayContainer.layer.cornerRadius = 4
dayContainer.isUserInteractionEnabled = false
dayLabel.font = .systemFont(ofSize: 14)
dayLabel.textAlignment = .center
dayContainer.addSubview(dayLabel)
addSubview(weekdayLabel)
addSubview(dayContainer)
weekdayLabel.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.height.equalTo(18)
}
dayContainer.snp.makeConstraints { make in
make.top.equalTo(weekdayLabel.snp.bottom).offset(12)
make.centerX.equalToSuperview()
make.width.height.equalTo(36)
make.bottom.equalToSuperview()
}
dayLabel.snp.makeConstraints { make in make.center.equalToSuperview() }
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(weekday: String, day: String, selected: Bool) {
weekdayLabel.text = weekday
dayLabel.text = day
dayLabel.textColor = selected ? .white : UIColor(hex: 0x4B5563)
dayContainer.backgroundColor = selected ? AppColor.primary : AppColor.inputBackground
accessibilityLabel = "星期\(weekday)\(day)"
accessibilityTraits = selected ? [.button, .selected] : .button
}
}
@ -983,6 +1173,17 @@ private final class ProfileTagWrapView: UIView {
private var itemViews: [UIView] = []
private var removeHandler: ((String) -> Void)?
private weak var trailingView: UIView?
func setTrailingView(_ view: UIView) {
trailingView = view
if view.superview !== self {
addSubview(view)
}
itemViews.append(view)
invalidateIntrinsicContentSize()
setNeedsLayout()
}
func setItems(_ items: [String], style: Style, removable: Bool, onRemove: ((String) -> Void)? = nil) {
subviews.forEach { $0.removeFromSuperview() }
@ -991,6 +1192,10 @@ private final class ProfileTagWrapView: UIView {
addSubview(view)
return view
}
if let trailingView {
addSubview(trailingView)
itemViews.append(trailingView)
}
removeHandler = onRemove
invalidateIntrinsicContentSize()
setNeedsLayout()
@ -1011,6 +1216,7 @@ private final class ProfileTagWrapView: UIView {
var y: CGFloat = 0
var rowHeight: CGFloat = 0
for view in itemViews {
guard !view.isHidden else { continue }
let size = view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if x > 0, x + size.width > width {
x = 0
@ -1030,6 +1236,7 @@ private final class ProfileTagWrapView: UIView {
var height: CGFloat = 0
var rowHeight: CGFloat = 0
for view in itemViews {
guard !view.isHidden else { continue }
let size = view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if x > 0, x + size.width > width {
x = 0
@ -1045,8 +1252,8 @@ private final class ProfileTagWrapView: UIView {
private func makeItem(title: String, style: Style, removable: Bool) -> UIView {
let button = UIButton(type: .system)
button.setTitle(removable ? "\(title) ×" : title, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: style == .certification ? 12 : 14)
button.contentEdgeInsets = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)
button.titleLabel?.font = .systemFont(ofSize: style == .device ? 14 : 12)
button.contentEdgeInsets = UIEdgeInsets(top: 2, left: style == .device ? 8 : 4, bottom: 2, right: style == .device ? 8 : 4)
button.layer.cornerRadius = 4
switch style {
case .label:
@ -1057,7 +1264,7 @@ private final class ProfileTagWrapView: UIView {
button.setTitleColor(AppColor.warning, for: .normal)
case .device:
button.backgroundColor = AppColor.inputBackground
button.setTitleColor(UIColor(hex: 0x565656), for: .normal)
button.setTitleColor(UIColor(hex: 0x4B5563), for: .normal)
}
if removable {
button.addAction(UIAction { [weak self] _ in
@ -1100,11 +1307,11 @@ private final class ProfileScheduleItemView: UIStackView {
let title = UILabel()
title.text = "\(schedule.startTime) - \(schedule.endTime) \(schedule.name)"
title.font = .systemFont(ofSize: 14)
title.textColor = UIColor(hex: 0x565656)
title.textColor = UIColor(hex: 0x4B5563)
title.numberOfLines = 1
let delete = UIButton(type: .system)
delete.setTitle("删除", for: .normal)
delete.setTitleColor(AppColor.danger, for: .normal)
delete.setTitleColor(UIColor(hex: 0xFF5656), for: .normal)
delete.titleLabel?.font = .systemFont(ofSize: 14)
delete.addAction(UIAction { [weak self] _ in self?.onDelete?() }, for: .touchUpInside)
top.addArrangedSubview(title)
@ -1136,7 +1343,7 @@ private final class ProfileScheduleItemView: UIStackView {
let label = UILabel()
label.text = text
label.font = .systemFont(ofSize: 14)
label.textColor = UIColor(hex: 0x565656)
label.textColor = UIColor(hex: 0x4B5563)
label.numberOfLines = 1
return label
}
@ -1151,7 +1358,7 @@ private final class ProfileScheduleItemView: UIStackView {
///
private enum ProfileAvatarPickerLoader {
static func loadImageData(from provider: NSItemProvider) async throws -> Data {
static func loadImage(from provider: NSItemProvider) async throws -> UIImage {
try await withCheckedThrowingContinuation { continuation in
guard provider.canLoadObject(ofClass: UIImage.self) else {
continuation.resume(throwing: OSSUploadError.unsupportedFileType)
@ -1162,11 +1369,11 @@ private enum ProfileAvatarPickerLoader {
continuation.resume(throwing: error)
return
}
guard let image = object as? UIImage, let data = image.jpegData(compressionQuality: 0.9) else {
guard let image = object as? UIImage else {
continuation.resume(throwing: OSSUploadError.emptyFile)
return
}
continuation.resume(returning: data)
continuation.resume(returning: image)
}
}
}

View File

@ -197,7 +197,7 @@ final class RealNameAuthViewController: BaseViewController {
}
private func submit() async {
let scenicId = AppStore.shared.currentScenicId
let scenicId = AppStore.shared.session.currentScenicId
guard scenicId > 0 else {
showToast("当前景区信息缺失")
return

View File

@ -221,7 +221,7 @@ final class WithdrawalSettingsViewController: BaseViewController {
}
private func submit() async {
let scenicId = AppStore.shared.currentScenicId
let scenicId = AppStore.shared.session.currentScenicId
guard scenicId > 0 else {
showToast("当前景区信息缺失")
return