模块化 AppStore 并完善素材管理与个人空间设置。
将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -150,7 +150,7 @@ final class BindAcquirerViewController: BaseViewController {
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
guard CooperationOrderFeature.hasPermission(in: AppStore.shared.permissionItems()) else {
|
||||
guard CooperationOrderFeature.hasPermission(in: AppStore.shared.permissions.permissionItems()) else {
|
||||
showToast("暂无合作订单权限")
|
||||
navigationController?.popViewController(animated: false)
|
||||
return
|
||||
|
||||
@ -76,7 +76,7 @@ final class CooperationAcquirerViewController: BaseViewController, UITableViewDa
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
guard CooperationOrderFeature.hasPermission(in: AppStore.shared.permissionItems()) else {
|
||||
guard CooperationOrderFeature.hasPermission(in: AppStore.shared.permissions.permissionItems()) else {
|
||||
showToast("暂无合作订单权限")
|
||||
navigationController?.popViewController(animated: false)
|
||||
return
|
||||
|
||||
@ -118,7 +118,7 @@ final class CooperationOrderListViewController: BaseViewController, UITableViewD
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
guard CooperationOrderFeature.hasPermission(in: AppStore.shared.permissionItems()) else {
|
||||
guard CooperationOrderFeature.hasPermission(in: AppStore.shared.permissions.permissionItems()) else {
|
||||
showToast("暂无合作订单权限")
|
||||
navigationController?.popViewController(animated: false)
|
||||
return
|
||||
|
||||
@ -201,7 +201,7 @@ final class HomeViewController: BaseViewController {
|
||||
for: indexPath
|
||||
) as! HomeQuickActionsCell
|
||||
cell.cardView.onCollectPayment = { [weak self] in
|
||||
guard AppStore.shared.currentScenicId > 0 else {
|
||||
guard AppStore.shared.session.currentScenicId > 0 else {
|
||||
self?.showToast("请先选择景区")
|
||||
return
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ final class AllFunctionMenuCell: UICollectionViewCell {
|
||||
|
||||
/// 更新菜单内容、操作状态及整张卡片的无障碍语义。
|
||||
func apply(menu: HomeMenuItem, actionStyle: ActionStyle) {
|
||||
iconView.image = UIImage(systemName: menu.iconName)
|
||||
iconView.image = UIImage(named: menu.iconName) ?? UIImage(systemName: menu.iconName)
|
||||
titleLabel.text = menu.title
|
||||
accessibilityLabel = menu.title
|
||||
|
||||
|
||||
@ -485,7 +485,7 @@ private final class AddLiveSheetViewController: UIViewController, PHPickerViewCo
|
||||
loadingIndicator.startAnimating()
|
||||
defer { loadingIndicator.stopAnimating() }
|
||||
do {
|
||||
let scenicId = AppStore.shared.currentScenicId
|
||||
let scenicId = AppStore.shared.session.currentScenicId
|
||||
let url = try await uploader.uploadLiveCover(
|
||||
data: media.data,
|
||||
fileName: media.fileName,
|
||||
|
||||
@ -37,10 +37,10 @@ final class LoginViewModel {
|
||||
/// 从本地存储恢复上次登录手机号与协议勾选状态。
|
||||
func applyStoredPreferences() {
|
||||
if account.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
||||
let lastUsername = AppStore.shared.lastLoginUsername {
|
||||
let lastUsername = AppStore.shared.session.lastLoginUsername {
|
||||
account = lastUsername
|
||||
}
|
||||
isPrivacyChecked = AppStore.shared.privacyAgreementAccepted
|
||||
isPrivacyChecked = AppStore.shared.session.privacyAgreementAccepted
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
|
||||
@ -146,17 +146,15 @@ final class MaterialDetailViewController: BaseViewController {
|
||||
}
|
||||
|
||||
@objc private func deleteTapped() {
|
||||
let alert = UIAlertController(title: "确认删除", message: "确定删除该素材吗?", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "删除", style: .destructive) { [weak self] _ in
|
||||
let dialog = MaterialDeleteDialogViewController(itemName: viewModel.detail?.name ?? "") { [weak self] in
|
||||
guard let self else { return }
|
||||
Task {
|
||||
self.showLoading()
|
||||
await self.viewModel.delete(api: self.api)
|
||||
self.hideLoading()
|
||||
}
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
present(dialog, animated: true)
|
||||
}
|
||||
|
||||
@objc private func editTapped() {
|
||||
@ -170,6 +168,28 @@ final class MaterialDetailViewController: BaseViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 素材详情无限轮播的页码映射规则。
|
||||
struct MaterialCarouselIndexMapper {
|
||||
/// 初始渲染页;多媒体时跳过头部复制页。
|
||||
static func initialRenderedPage(mediaCount: Int) -> Int {
|
||||
mediaCount > 1 ? 1 : 0
|
||||
}
|
||||
|
||||
/// 将头尾复制页归一到对应的真实渲染页。
|
||||
static func normalizedRenderedPage(_ page: Int, mediaCount: Int) -> Int {
|
||||
guard mediaCount > 1 else { return 0 }
|
||||
if page <= 0 { return mediaCount }
|
||||
if page >= mediaCount + 1 { return 1 }
|
||||
return page
|
||||
}
|
||||
|
||||
/// 将渲染页映射到圆点使用的真实素材索引。
|
||||
static func logicalPage(renderedPage: Int, mediaCount: Int) -> Int {
|
||||
guard mediaCount > 1 else { return 0 }
|
||||
return normalizedRenderedPage(renderedPage, mediaCount: mediaCount) - 1
|
||||
}
|
||||
}
|
||||
|
||||
/// 素材详情媒体轮播,支持图片自动切换与视频播完切换。
|
||||
/// 素材详情顶部媒体轮播视图,支持图片与视频播放。
|
||||
private final class MaterialMediaCarouselView: UIView, UIScrollViewDelegate {
|
||||
@ -177,8 +197,11 @@ private final class MaterialMediaCarouselView: UIView, UIScrollViewDelegate {
|
||||
private let pageControl = UIPageControl()
|
||||
private let placeholderLabel = UILabel()
|
||||
private var media: [MaterialDetailMediaItem] = []
|
||||
private var renderedMedia: [MaterialDetailMediaItem] = []
|
||||
private var players: [Int: AVPlayer] = [:]
|
||||
private var playerViews: [Int: MaterialPlayerLayerView] = [:]
|
||||
private var timer: Timer?
|
||||
private var renderedPage = 0
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
@ -224,7 +247,9 @@ private final class MaterialMediaCarouselView: UIView, UIScrollViewDelegate {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
players.values.forEach { $0.pause() }
|
||||
players.removeAll()
|
||||
playerViews.removeAll()
|
||||
self.media = media
|
||||
renderedMedia = media.count > 1 ? [media.last!] + media + [media.first!] : media
|
||||
scrollView.subviews.forEach { $0.removeFromSuperview() }
|
||||
pageControl.numberOfPages = media.count
|
||||
pageControl.currentPage = 0
|
||||
@ -233,7 +258,8 @@ private final class MaterialMediaCarouselView: UIView, UIScrollViewDelegate {
|
||||
scrollView.isHidden = media.isEmpty
|
||||
guard !media.isEmpty else { return }
|
||||
|
||||
for (index, item) in media.enumerated() {
|
||||
renderedPage = MaterialCarouselIndexMapper.initialRenderedPage(mediaCount: media.count)
|
||||
for (index, item) in renderedMedia.enumerated() {
|
||||
let page = UIView()
|
||||
page.backgroundColor = UIColor(hex: 0xF5F5F5)
|
||||
scrollView.addSubview(page)
|
||||
@ -245,6 +271,7 @@ private final class MaterialMediaCarouselView: UIView, UIScrollViewDelegate {
|
||||
}
|
||||
setNeedsLayout()
|
||||
layoutIfNeeded()
|
||||
scrollView.contentOffset = CGPoint(x: CGFloat(renderedPage) * bounds.width, y: 0)
|
||||
playCurrentPageIfNeeded()
|
||||
}
|
||||
|
||||
@ -283,7 +310,8 @@ private final class MaterialMediaCarouselView: UIView, UIScrollViewDelegate {
|
||||
let player = AVPlayer(url: url)
|
||||
players[index] = player
|
||||
let playerView = MaterialPlayerLayerView()
|
||||
playerView.playerLayer.player = player
|
||||
playerView.configure(player: player)
|
||||
playerViews[index] = playerView
|
||||
page.addSubview(playerView)
|
||||
playerView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
@ -302,29 +330,40 @@ private final class MaterialMediaCarouselView: UIView, UIScrollViewDelegate {
|
||||
for (index, page) in scrollView.subviews.enumerated() {
|
||||
page.frame = CGRect(x: CGFloat(index) * size.width, y: 0, width: size.width, height: size.height)
|
||||
}
|
||||
scrollView.contentSize = CGSize(width: size.width * CGFloat(media.count), height: size.height)
|
||||
scrollView.contentOffset = CGPoint(x: CGFloat(pageControl.currentPage) * size.width, y: 0)
|
||||
scrollView.contentSize = CGSize(width: size.width * CGFloat(renderedMedia.count), height: size.height)
|
||||
scrollView.contentOffset = CGPoint(x: CGFloat(renderedPage) * size.width, y: 0)
|
||||
}
|
||||
|
||||
private func updateCurrentPage() {
|
||||
guard bounds.width > 0 else { return }
|
||||
let page = Int(round(scrollView.contentOffset.x / bounds.width))
|
||||
pageControl.currentPage = max(0, min(page, media.count - 1))
|
||||
let rawPage = Int(round(scrollView.contentOffset.x / bounds.width))
|
||||
let page = MaterialCarouselIndexMapper.normalizedRenderedPage(rawPage, mediaCount: media.count)
|
||||
if media.count > 1 {
|
||||
if page != rawPage {
|
||||
scrollView.setContentOffset(CGPoint(x: CGFloat(page) * bounds.width, y: 0), animated: false)
|
||||
}
|
||||
pageControl.currentPage = MaterialCarouselIndexMapper.logicalPage(renderedPage: page, mediaCount: media.count)
|
||||
} else {
|
||||
pageControl.currentPage = 0
|
||||
}
|
||||
renderedPage = page
|
||||
playCurrentPageIfNeeded()
|
||||
}
|
||||
|
||||
private func playCurrentPageIfNeeded() {
|
||||
stopTimer()
|
||||
players.forEach { index, player in
|
||||
if index == pageControl.currentPage {
|
||||
if index == renderedPage {
|
||||
player.seek(to: .zero)
|
||||
player.play()
|
||||
playerViews[index]?.setPlaying(true)
|
||||
} else {
|
||||
player.pause()
|
||||
playerViews[index]?.setPlaying(false)
|
||||
}
|
||||
}
|
||||
guard !media.isEmpty else { return }
|
||||
if media[pageControl.currentPage].type == 1 {
|
||||
guard renderedMedia.indices.contains(renderedPage) else { return }
|
||||
if renderedMedia[renderedPage].type == 1 {
|
||||
startImageTimer()
|
||||
}
|
||||
}
|
||||
@ -342,13 +381,13 @@ private final class MaterialMediaCarouselView: UIView, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
@objc private func videoDidEnd(_ notification: Notification) {
|
||||
guard players[renderedPage]?.currentItem === notification.object as? AVPlayerItem else { return }
|
||||
advancePage()
|
||||
}
|
||||
|
||||
private func advancePage() {
|
||||
guard media.count > 1, bounds.width > 0 else { return }
|
||||
let nextPage = (pageControl.currentPage + 1) % media.count
|
||||
pageControl.currentPage = nextPage
|
||||
let nextPage = renderedPage + 1
|
||||
scrollView.setContentOffset(CGPoint(x: CGFloat(nextPage) * bounds.width, y: 0), animated: true)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
|
||||
self?.playCurrentPageIfNeeded()
|
||||
@ -361,6 +400,64 @@ private final class MaterialMediaCarouselView: UIView, UIScrollViewDelegate {
|
||||
private final class MaterialPlayerLayerView: UIView {
|
||||
override class var layerClass: AnyClass { AVPlayerLayer.self }
|
||||
var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
|
||||
private let controlButton = UIButton(type: .system)
|
||||
private weak var player: AVPlayer?
|
||||
private var hideWorkItem: DispatchWorkItem?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
controlButton.tintColor = .white
|
||||
controlButton.backgroundColor = UIColor.black.withAlphaComponent(0.5)
|
||||
controlButton.layer.cornerRadius = 24
|
||||
controlButton.addTarget(self, action: #selector(controlTapped), for: .touchUpInside)
|
||||
addSubview(controlButton)
|
||||
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showControls)))
|
||||
controlButton.snp.makeConstraints { make in make.center.equalToSuperview(); make.size.equalTo(48) }
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
func configure(player: AVPlayer) {
|
||||
self.player = player
|
||||
playerLayer.player = player
|
||||
playerLayer.videoGravity = .resizeAspect
|
||||
setPlaying(false)
|
||||
}
|
||||
|
||||
func setPlaying(_ playing: Bool) {
|
||||
controlButton.setImage(UIImage(systemName: playing ? "pause.fill" : "play.fill"), for: .normal)
|
||||
if playing {
|
||||
scheduleHide()
|
||||
} else {
|
||||
hideWorkItem?.cancel()
|
||||
controlButton.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func controlTapped() {
|
||||
guard let player else { return }
|
||||
if player.timeControlStatus == .playing {
|
||||
player.pause()
|
||||
setPlaying(false)
|
||||
} else {
|
||||
player.play()
|
||||
setPlaying(true)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func showControls() {
|
||||
controlButton.isHidden = false
|
||||
if player?.timeControlStatus == .playing { scheduleHide() }
|
||||
}
|
||||
|
||||
private func scheduleHide() {
|
||||
hideWorkItem?.cancel()
|
||||
controlButton.isHidden = false
|
||||
let work = DispatchWorkItem { [weak self] in self?.controlButton.isHidden = true }
|
||||
hideWorkItem = work
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: work)
|
||||
}
|
||||
}
|
||||
|
||||
/// 素材详情基础信息区。
|
||||
@ -370,7 +467,7 @@ private final class MaterialDetailInfoSection: UIView {
|
||||
private let listingBadge = UILabel()
|
||||
private let tagStack = UIStackView()
|
||||
private let statsStack = UIStackView()
|
||||
private let downloadStat = MaterialDetailStatView(systemIconName: "arrow.down.circle")
|
||||
private let downloadStat = MaterialDetailStatView(iconName: "material_ic_download_count")
|
||||
private let likeStat = MaterialDetailStatView(iconName: "sample_ic_like_count")
|
||||
private let collectStat = MaterialDetailStatView(iconName: "sample_ic_collect_count")
|
||||
|
||||
|
||||
@ -41,6 +41,7 @@ final class MaterialFormViewController: BaseViewController {
|
||||
private let cancelButton = UIButton(type: .system)
|
||||
private let confirmButton = UIButton(type: .system)
|
||||
private let progressOverlay = MaterialUploadProgressOverlay()
|
||||
private let dropdownOverlay = MaterialAnchoredDropdownView()
|
||||
|
||||
private var pickTarget: PickTarget = .material
|
||||
var onSubmitSuccess: (() -> Void)?
|
||||
@ -106,7 +107,9 @@ final class MaterialFormViewController: BaseViewController {
|
||||
bottomBar.addSubview(cancelButton)
|
||||
bottomBar.addSubview(confirmButton)
|
||||
view.addSubview(progressOverlay)
|
||||
view.addSubview(dropdownOverlay)
|
||||
progressOverlay.isHidden = true
|
||||
dropdownOverlay.isHidden = true
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
@ -147,6 +150,9 @@ final class MaterialFormViewController: BaseViewController {
|
||||
progressOverlay.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
dropdownOverlay.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
override func bindActions() {
|
||||
@ -167,6 +173,7 @@ final class MaterialFormViewController: BaseViewController {
|
||||
mediaTypeButton.addTarget(self, action: #selector(mediaTypeTapped), for: .touchUpInside)
|
||||
mediaGridView.onAdd = { [weak self] in self?.presentPickerSource(target: .material) }
|
||||
mediaGridView.onDelete = { [weak self] index in self?.viewModel.deleteMaterial(at: index) }
|
||||
mediaGridView.onPreview = { [weak self] index in self?.presentMediaPreview(startingAt: index) }
|
||||
coverView.onAdd = { [weak self] in self?.presentPickerSource(target: .cover) }
|
||||
coverView.onDelete = { [weak self] in self?.viewModel.deleteCoverImage() }
|
||||
tagView.onTextChanged = { [weak self] text in self?.viewModel.updateTagInput(text) }
|
||||
@ -174,7 +181,7 @@ final class MaterialFormViewController: BaseViewController {
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.addTag(api: self.api) }
|
||||
}
|
||||
tagView.onDelete = { [weak self] tag in self?.viewModel.deleteTag(tag) }
|
||||
tagView.onDelete = { [weak self] tag in self?.viewModel.requestDeleteTag(tag) }
|
||||
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
|
||||
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
|
||||
applyViewModel()
|
||||
@ -207,6 +214,7 @@ final class MaterialFormViewController: BaseViewController {
|
||||
} else {
|
||||
progressOverlay.isHidden = true
|
||||
}
|
||||
presentTagDeleteDialogIfNeeded()
|
||||
}
|
||||
|
||||
private func configureBottomButton(_ button: UIButton, title: String, backgroundColor: UIColor, titleColor: UIColor) {
|
||||
@ -222,25 +230,30 @@ final class MaterialFormViewController: BaseViewController {
|
||||
}
|
||||
|
||||
@objc private func spotTapped() {
|
||||
let sheet = UIAlertController(title: "关联打卡点", message: nil, preferredStyle: .actionSheet)
|
||||
viewModel.scenicSpotList.forEach { spot in
|
||||
sheet.addAction(UIAlertAction(title: spot.name, style: .default) { [weak self] _ in
|
||||
self?.viewModel.selectScenicSpot(spot)
|
||||
})
|
||||
let spots = viewModel.scenicSpotList
|
||||
dropdownOverlay.show(
|
||||
in: view,
|
||||
anchor: spotButton,
|
||||
options: spots.map(\.name),
|
||||
selectedIndex: spots.firstIndex { $0.id == viewModel.selectedScenicSpot?.id },
|
||||
maximumHeight: 128
|
||||
) { [weak self] index in
|
||||
guard spots.indices.contains(index) else { return }
|
||||
self?.viewModel.selectScenicSpot(spots[index])
|
||||
}
|
||||
sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
present(sheet, animated: true)
|
||||
}
|
||||
|
||||
@objc private func mediaTypeTapped() {
|
||||
let sheet = UIAlertController(title: "素材类型", message: nil, preferredStyle: .actionSheet)
|
||||
MaterialMediaType.allCases.forEach { type in
|
||||
sheet.addAction(UIAlertAction(title: type.title, style: .default) { [weak self] _ in
|
||||
self?.viewModel.selectMediaType(type)
|
||||
})
|
||||
let types = MaterialMediaType.allCases
|
||||
dropdownOverlay.show(
|
||||
in: view,
|
||||
anchor: mediaTypeButton,
|
||||
options: types.map(\.title),
|
||||
selectedIndex: types.firstIndex(of: viewModel.mediaType)
|
||||
) { [weak self] index in
|
||||
guard types.indices.contains(index) else { return }
|
||||
self?.viewModel.selectMediaType(types[index])
|
||||
}
|
||||
sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
present(sheet, animated: true)
|
||||
}
|
||||
|
||||
@objc private func cancelTapped() {
|
||||
@ -267,24 +280,29 @@ final class MaterialFormViewController: BaseViewController {
|
||||
sheet.addAction(UIAlertAction(title: "从相册选择", style: .default) { [weak self] _ in
|
||||
self?.presentPhotoPicker()
|
||||
})
|
||||
if target == .material {
|
||||
sheet.addAction(UIAlertAction(title: "从云盘导入", style: .default) { [weak self] _ in
|
||||
self?.openCloudPicker()
|
||||
})
|
||||
}
|
||||
sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
present(sheet, animated: true)
|
||||
}
|
||||
|
||||
private func openCloudPicker() {
|
||||
let controller = CloudStoragePickForTaskViewController(
|
||||
importedFileIDs: viewModel.importedCloudFileIDs,
|
||||
allowedFileType: viewModel.mediaType.cloudFileType
|
||||
)
|
||||
controller.onConfirmed = { [weak self] files in
|
||||
self?.viewModel.addCloudMediaFiles(files)
|
||||
private func presentMediaPreview(startingAt sourceIndex: Int) {
|
||||
let completed = viewModel.uploadedMediaList.enumerated().filter { !$0.element.isUploading && !$0.element.ossUrl.isEmpty }
|
||||
guard let startIndex = completed.firstIndex(where: { $0.offset == sourceIndex }) else { return }
|
||||
let items = completed.map {
|
||||
MaterialPreviewItem(url: $0.element.ossUrl, mediaType: viewModel.mediaType)
|
||||
}
|
||||
navigationController?.pushViewController(controller, animated: true)
|
||||
present(MaterialMediaPreviewViewController(items: items, startIndex: startIndex), animated: true)
|
||||
}
|
||||
|
||||
private func presentTagDeleteDialogIfNeeded() {
|
||||
guard let tag = viewModel.pendingDeleteTag, presentedViewController == nil else { return }
|
||||
let dialog = MaterialDeleteDialogViewController(
|
||||
title: "删除标签",
|
||||
itemName: tag.name,
|
||||
itemSuffix: "标签吗?",
|
||||
onCancel: { [weak self] in self?.viewModel.cancelDeleteTag() },
|
||||
onConfirm: { [weak self] in self?.viewModel.confirmDeleteTag() }
|
||||
)
|
||||
present(dialog, animated: true)
|
||||
}
|
||||
|
||||
private func presentPhotoPicker() {
|
||||
@ -462,7 +480,7 @@ private final class MaterialFormFieldView: UIView {
|
||||
/// 带标题和值的表单选择控件。
|
||||
private final class MaterialSelectButton: UIControl {
|
||||
private let titleLabel = UILabel()
|
||||
private let chevronView = UIImageView(image: UIImage(systemName: "chevron.down"))
|
||||
private let chevronView = UIImageView(image: UIImage(named: "material_ic_arrow_down"))
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
@ -497,12 +515,92 @@ private final class MaterialSelectButton: UIControl {
|
||||
}
|
||||
}
|
||||
|
||||
/// 锚定在表单选择框下方的 Android 风格下拉菜单。
|
||||
private final class MaterialAnchoredDropdownView: UIControl {
|
||||
private let cardView = UIView()
|
||||
private let scrollView = UIScrollView()
|
||||
private let stackView = UIStackView()
|
||||
private var onSelect: ((Int) -> Void)?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .clear
|
||||
cardView.backgroundColor = .white
|
||||
cardView.layer.cornerRadius = 4
|
||||
cardView.layer.shadowColor = UIColor.black.cgColor
|
||||
cardView.layer.shadowOpacity = 0.14
|
||||
cardView.layer.shadowRadius = 8
|
||||
cardView.layer.shadowOffset = CGSize(width: 0, height: 4)
|
||||
stackView.axis = .vertical
|
||||
addSubview(cardView)
|
||||
cardView.addSubview(scrollView)
|
||||
scrollView.addSubview(stackView)
|
||||
addTarget(self, action: #selector(dismissMenu), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 在指定控件下方展示菜单。
|
||||
func show(
|
||||
in container: UIView,
|
||||
anchor: UIView,
|
||||
options: [String],
|
||||
selectedIndex: Int?,
|
||||
maximumHeight: CGFloat = 128,
|
||||
onSelect: @escaping (Int) -> Void
|
||||
) {
|
||||
guard !options.isEmpty else { return }
|
||||
self.onSelect = onSelect
|
||||
stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
for (index, option) in options.enumerated() {
|
||||
let button = UIButton(type: .system)
|
||||
button.tag = index
|
||||
button.contentHorizontalAlignment = .leading
|
||||
button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)
|
||||
button.setTitle(option, for: .normal)
|
||||
button.setTitleColor(index == selectedIndex ? AppColor.primary : .black, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 14, weight: index == selectedIndex ? .medium : .regular)
|
||||
button.backgroundColor = .white
|
||||
button.addTarget(self, action: #selector(optionTapped(_:)), for: .touchUpInside)
|
||||
button.snp.makeConstraints { make in make.height.equalTo(28) }
|
||||
stackView.addArrangedSubview(button)
|
||||
}
|
||||
|
||||
isHidden = false
|
||||
container.bringSubviewToFront(self)
|
||||
layoutIfNeeded()
|
||||
let anchorFrame = anchor.convert(anchor.bounds, to: self)
|
||||
let menuHeight = min(maximumHeight, CGFloat(options.count) * 28)
|
||||
cardView.frame = CGRect(x: anchorFrame.minX, y: anchorFrame.maxY, width: anchorFrame.width, height: menuHeight)
|
||||
scrollView.frame = cardView.bounds
|
||||
stackView.frame = CGRect(x: 0, y: 0, width: cardView.bounds.width, height: CGFloat(options.count) * 28)
|
||||
scrollView.contentSize = stackView.frame.size
|
||||
}
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
isHidden ? false : true
|
||||
}
|
||||
|
||||
@objc private func optionTapped(_ sender: UIButton) {
|
||||
isHidden = true
|
||||
onSelect?(sender.tag)
|
||||
}
|
||||
|
||||
@objc private func dismissMenu() {
|
||||
isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
/// 素材上传网格。
|
||||
/// 素材媒体上传网格视图。
|
||||
private final class MaterialUploadGridView: UIView {
|
||||
private let stack = UIStackView()
|
||||
var onAdd: (() -> Void)?
|
||||
var onDelete: ((Int) -> Void)?
|
||||
var onPreview: ((Int) -> Void)?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
@ -550,6 +648,7 @@ private final class MaterialUploadGridView: UIView {
|
||||
if let item {
|
||||
cell.apply(item: item)
|
||||
cell.onDelete = { [weak self] in self?.onDelete?(index) }
|
||||
cell.onPreview = { [weak self] in self?.onPreview?(index) }
|
||||
} else {
|
||||
cell.applyAdd()
|
||||
cell.onAdd = { [weak self] in self?.onAdd?() }
|
||||
@ -563,14 +662,17 @@ private final class MaterialUploadGridView: UIView {
|
||||
private final class MaterialUploadThumbView: UIControl {
|
||||
private let imageView = UIImageView()
|
||||
private let addCircle = UIView()
|
||||
private let addIcon = UIImageView(image: UIImage(systemName: "plus"))
|
||||
private let addIcon = UIImageView(image: UIImage(named: "material_ic_add"))
|
||||
private let textLabel = UILabel()
|
||||
private let deleteButton = UIButton(type: .system)
|
||||
private let playIcon = UIImageView(image: UIImage(systemName: "play.fill"))
|
||||
private let playIcon = UIImageView(image: UIImage(named: "material_ic_play"))
|
||||
private let mediaType: MaterialMediaType
|
||||
private let dashedLayer = CAShapeLayer()
|
||||
private var previewEnabled = false
|
||||
|
||||
var onAdd: (() -> Void)?
|
||||
var onDelete: (() -> Void)?
|
||||
var onPreview: (() -> Void)?
|
||||
|
||||
init(mediaType: MaterialMediaType) {
|
||||
self.mediaType = mediaType
|
||||
@ -586,7 +688,7 @@ private final class MaterialUploadThumbView: UIControl {
|
||||
textLabel.font = .systemFont(ofSize: 12)
|
||||
textLabel.textColor = UIColor(hex: 0x4B5563)
|
||||
textLabel.textAlignment = .center
|
||||
deleteButton.setImage(UIImage(systemName: "xmark"), for: .normal)
|
||||
deleteButton.setImage(UIImage(named: "material_ic_clear"), for: .normal)
|
||||
deleteButton.tintColor = .white
|
||||
deleteButton.backgroundColor = UIColor.black.withAlphaComponent(0.6)
|
||||
deleteButton.layer.cornerRadius = 10
|
||||
@ -622,7 +724,12 @@ private final class MaterialUploadThumbView: UIControl {
|
||||
make.center.equalToSuperview()
|
||||
make.size.equalTo(32)
|
||||
}
|
||||
addTarget(self, action: #selector(addTapped), for: .touchUpInside)
|
||||
dashedLayer.strokeColor = UIColor(hex: 0xE5E7EB).cgColor
|
||||
dashedLayer.fillColor = UIColor.clear.cgColor
|
||||
dashedLayer.lineWidth = 1
|
||||
dashedLayer.lineDashPattern = [6, 6]
|
||||
layer.addSublayer(dashedLayer)
|
||||
addTarget(self, action: #selector(cellTapped), for: .touchUpInside)
|
||||
deleteButton.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@ -631,12 +738,19 @@ private final class MaterialUploadThumbView: UIControl {
|
||||
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: 8).cgPath
|
||||
}
|
||||
|
||||
func applyAdd() {
|
||||
imageView.image = nil
|
||||
imageView.kf.cancelDownloadTask()
|
||||
backgroundColor = .white
|
||||
layer.borderColor = UIColor(hex: 0xE5E7EB).cgColor
|
||||
layer.borderWidth = 1
|
||||
layer.borderWidth = 0
|
||||
dashedLayer.isHidden = false
|
||||
previewEnabled = false
|
||||
addCircle.isHidden = false
|
||||
textLabel.isHidden = false
|
||||
textLabel.text = mediaType == .image ? "点击上传图片" : "点击上传视频"
|
||||
@ -646,7 +760,8 @@ private final class MaterialUploadThumbView: UIControl {
|
||||
}
|
||||
|
||||
func apply(item: MaterialUploadedMediaItem) {
|
||||
layer.borderWidth = 0
|
||||
dashedLayer.isHidden = true
|
||||
previewEnabled = !item.ossUrl.isEmpty && !item.isUploading
|
||||
backgroundColor = UIColor(hex: 0xF5F5F5)
|
||||
addCircle.isHidden = true
|
||||
deleteButton.isHidden = item.ossUrl.isEmpty || item.isUploading
|
||||
@ -658,7 +773,14 @@ private final class MaterialUploadThumbView: UIControl {
|
||||
textLabel.numberOfLines = 2
|
||||
} else {
|
||||
textLabel.isHidden = true
|
||||
if let urlText = item.previewURL, let url = URL(string: urlText), !urlText.isEmpty {
|
||||
if mediaType == .video {
|
||||
if let thumbnailData = item.thumbnailData, let image = UIImage(data: thumbnailData) {
|
||||
imageView.image = image
|
||||
} else if let urlText = (item.previewURL?.isEmpty == false ? item.previewURL : item.ossUrl),
|
||||
!urlText.isEmpty, let url = URL(string: urlText) {
|
||||
loadVideoThumbnail(url: url)
|
||||
}
|
||||
} else if let urlText = item.previewURL, let url = URL(string: urlText), !urlText.isEmpty {
|
||||
imageView.kf.setImage(with: url)
|
||||
} else {
|
||||
imageView.image = UIImage(data: item.thumbnailData ?? item.data)
|
||||
@ -666,14 +788,27 @@ private final class MaterialUploadThumbView: UIControl {
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func addTapped() {
|
||||
guard !addCircle.isHidden else { return }
|
||||
onAdd?()
|
||||
@objc private func cellTapped() {
|
||||
if !addCircle.isHidden {
|
||||
onAdd?()
|
||||
} else if previewEnabled {
|
||||
onPreview?()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func deleteTapped() {
|
||||
onDelete?()
|
||||
}
|
||||
|
||||
private func loadVideoThumbnail(url: URL) {
|
||||
imageView.image = nil
|
||||
let generator = AVAssetImageGenerator(asset: AVURLAsset(url: url))
|
||||
generator.appliesPreferredTrackTransform = true
|
||||
generator.generateCGImagesAsynchronously(forTimes: [NSValue(time: .zero)]) { [weak self] _, image, _, _, _ in
|
||||
guard let image else { return }
|
||||
DispatchQueue.main.async { self?.imageView.image = UIImage(cgImage: image) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 素材封面上传视图。
|
||||
@ -681,18 +816,22 @@ private final class MaterialUploadThumbView: UIControl {
|
||||
private final class MaterialCoverUploadView: UIControl {
|
||||
private let imageView = UIImageView()
|
||||
private let addCircle = UIView()
|
||||
private let addIcon = UIImageView(image: UIImage(systemName: "plus"))
|
||||
private let addIcon = UIImageView(image: UIImage(named: "material_ic_add"))
|
||||
private let titleLabel = UILabel()
|
||||
private let hintLabel = UILabel()
|
||||
private let deleteButton = UIButton(type: .system)
|
||||
private let dashedLayer = CAShapeLayer()
|
||||
var onAdd: (() -> Void)?
|
||||
var onDelete: (() -> Void)?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
layer.cornerRadius = 8
|
||||
layer.borderColor = UIColor(hex: 0xE5E7EB).cgColor
|
||||
layer.borderWidth = 1
|
||||
dashedLayer.strokeColor = UIColor(hex: 0xE5E7EB).cgColor
|
||||
dashedLayer.fillColor = UIColor.clear.cgColor
|
||||
dashedLayer.lineWidth = 1
|
||||
dashedLayer.lineDashPattern = [6, 6]
|
||||
layer.addSublayer(dashedLayer)
|
||||
clipsToBounds = true
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.clipsToBounds = true
|
||||
@ -707,7 +846,7 @@ private final class MaterialCoverUploadView: UIControl {
|
||||
hintLabel.font = .systemFont(ofSize: 12)
|
||||
hintLabel.textColor = UIColor(hex: 0xB6BECA)
|
||||
hintLabel.textAlignment = .center
|
||||
deleteButton.setImage(UIImage(systemName: "xmark"), for: .normal)
|
||||
deleteButton.setImage(UIImage(named: "material_ic_clear"), for: .normal)
|
||||
deleteButton.tintColor = .white
|
||||
deleteButton.backgroundColor = UIColor.black.withAlphaComponent(0.6)
|
||||
deleteButton.layer.cornerRadius = 12
|
||||
@ -753,6 +892,12 @@ private final class MaterialCoverUploadView: UIControl {
|
||||
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: 8).cgPath
|
||||
}
|
||||
|
||||
func apply(item: MaterialUploadedMediaItem?) {
|
||||
guard let item else {
|
||||
imageView.image = nil
|
||||
@ -763,6 +908,7 @@ private final class MaterialCoverUploadView: UIControl {
|
||||
titleLabel.numberOfLines = 1
|
||||
hintLabel.isHidden = false
|
||||
deleteButton.isHidden = true
|
||||
dashedLayer.isHidden = false
|
||||
return
|
||||
}
|
||||
if item.isUploading {
|
||||
@ -773,6 +919,7 @@ private final class MaterialCoverUploadView: UIControl {
|
||||
titleLabel.numberOfLines = 2
|
||||
hintLabel.isHidden = true
|
||||
deleteButton.isHidden = true
|
||||
dashedLayer.isHidden = true
|
||||
} else {
|
||||
if let urlText = item.previewURL, let url = URL(string: urlText), !urlText.isEmpty {
|
||||
imageView.kf.setImage(with: url)
|
||||
@ -783,6 +930,7 @@ private final class MaterialCoverUploadView: UIControl {
|
||||
titleLabel.isHidden = true
|
||||
hintLabel.isHidden = true
|
||||
deleteButton.isHidden = item.ossUrl.isEmpty
|
||||
dashedLayer.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -819,17 +967,17 @@ private final class MaterialTagEditorView: UIView {
|
||||
tagsStack.alignment = .leading
|
||||
inputRow.axis = .horizontal
|
||||
inputRow.spacing = 8
|
||||
textField.placeholder = "请输入标签"
|
||||
textField.font = .systemFont(ofSize: 14)
|
||||
textField.layer.borderColor = UIColor(hex: 0xEEEEEE).cgColor
|
||||
textField.layer.borderWidth = 1
|
||||
textField.layer.cornerRadius = 8
|
||||
textField.layer.cornerRadius = 4
|
||||
textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 44))
|
||||
textField.leftViewMode = .always
|
||||
addButton.setTitle("添加", for: .normal)
|
||||
textField.placeholder = "请输入标签,最低5个字符"
|
||||
addButton.setTitle("添加标签", for: .normal)
|
||||
addButton.setTitleColor(.white, for: .normal)
|
||||
addButton.backgroundColor = AppColor.primary
|
||||
addButton.layer.cornerRadius = 8
|
||||
addButton.layer.cornerRadius = 4
|
||||
addSubview(stack)
|
||||
stack.addArrangedSubview(tagsStack)
|
||||
stack.addArrangedSubview(inputRow)
|
||||
@ -839,11 +987,11 @@ private final class MaterialTagEditorView: UIView {
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
textField.snp.makeConstraints { make in
|
||||
make.height.equalTo(44)
|
||||
make.height.equalTo(36)
|
||||
}
|
||||
addButton.snp.makeConstraints { make in
|
||||
make.width.equalTo(72)
|
||||
make.height.equalTo(44)
|
||||
make.width.equalTo(80)
|
||||
make.height.equalTo(36)
|
||||
}
|
||||
textField.addTarget(self, action: #selector(textChanged), for: .editingChanged)
|
||||
addButton.addTarget(self, action: #selector(addTapped), for: .touchUpInside)
|
||||
@ -859,10 +1007,13 @@ private final class MaterialTagEditorView: UIView {
|
||||
if textField.text != input {
|
||||
textField.text = input
|
||||
}
|
||||
let canAdd = (5 ... 15).contains(input.count)
|
||||
addButton.isEnabled = canAdd
|
||||
addButton.backgroundColor = canAdd ? AppColor.primary : AppColor.buttonDisabled
|
||||
tagsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
tags.forEach { tag in
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitle("\(tag.name) ×", for: .normal)
|
||||
button.setTitle(tag.name, for: .normal)
|
||||
button.setTitleColor(AppColor.primary, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 12)
|
||||
button.backgroundColor = UIColor(hex: 0xEFF6FF)
|
||||
|
||||
@ -15,16 +15,15 @@ final class MaterialListViewController: BaseViewController {
|
||||
|
||||
private let searchContainer = UIView()
|
||||
private let searchBox = UIView()
|
||||
private let searchIconView = UIImageView(image: UIImage(systemName: "magnifyingglass"))
|
||||
private let searchIconView = UIImageView(image: UIImage(named: "material_ic_search"))
|
||||
private let searchTextField = UITextField()
|
||||
private let filterContainer = UIView()
|
||||
private let filterButton = UIButton(type: .system)
|
||||
private let filterTitleLabel = UILabel()
|
||||
private let filterChevronView = UIImageView(image: UIImage(systemName: "chevron.down"))
|
||||
private let filterChevronView = UIImageView(image: UIImage(named: "material_ic_arrow_down"))
|
||||
private let filterDropdownView = UIView()
|
||||
private let filterDropdownStack = UIStackView()
|
||||
private let tableView = UITableView(frame: .zero, style: .plain)
|
||||
private let emptyLabel = UILabel()
|
||||
private let bottomBar = UIView()
|
||||
private let addButton = UIButton(type: .system)
|
||||
private var dataSource: UITableViewDiffableDataSource<Int, MaterialListRow>!
|
||||
@ -115,12 +114,6 @@ final class MaterialListViewController: BaseViewController {
|
||||
}
|
||||
}
|
||||
|
||||
emptyLabel.text = "暂无素材"
|
||||
emptyLabel.font = .systemFont(ofSize: 16)
|
||||
emptyLabel.textColor = AppColor.textSecondary
|
||||
emptyLabel.textAlignment = .center
|
||||
emptyLabel.isHidden = true
|
||||
|
||||
bottomBar.backgroundColor = .white
|
||||
addButton.setTitle("添加", for: .normal)
|
||||
addButton.setTitleColor(.white, for: .normal)
|
||||
@ -137,7 +130,6 @@ final class MaterialListViewController: BaseViewController {
|
||||
filterButton.addSubview(filterTitleLabel)
|
||||
filterButton.addSubview(filterChevronView)
|
||||
view.addSubview(tableView)
|
||||
view.addSubview(emptyLabel)
|
||||
view.addSubview(bottomBar)
|
||||
bottomBar.addSubview(addButton)
|
||||
view.addSubview(filterDropdownView)
|
||||
@ -206,11 +198,6 @@ final class MaterialListViewController: BaseViewController {
|
||||
filterDropdownStack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
emptyLabel.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.centerY.equalTo(tableView)
|
||||
make.leading.trailing.equalToSuperview().inset(24)
|
||||
}
|
||||
}
|
||||
|
||||
override func bindActions() {
|
||||
@ -242,7 +229,7 @@ final class MaterialListViewController: BaseViewController {
|
||||
|
||||
@MainActor
|
||||
private func applyViewModel() {
|
||||
filterTitleLabel.text = viewModel.filterStatus == .all ? "筛选" : viewModel.filterStatus.title
|
||||
filterTitleLabel.text = "筛选"
|
||||
filterOptionButtons.forEach { status, button in
|
||||
let selected = status == viewModel.filterStatus
|
||||
button.setTitleColor(selected ? AppColor.primary : AppColor.textPrimary, for: .normal)
|
||||
@ -256,7 +243,6 @@ final class MaterialListViewController: BaseViewController {
|
||||
snapshot.appendSections([0])
|
||||
snapshot.appendItems([.stats(viewModel.orderInfo)] + viewModel.items.map(MaterialListRow.item))
|
||||
dataSource.apply(snapshot, animatingDifferences: true)
|
||||
emptyLabel.isHidden = !viewModel.items.isEmpty || viewModel.isLoading || viewModel.isRefreshing
|
||||
viewModel.isLoading && viewModel.items.isEmpty ? showLoading() : hideLoading()
|
||||
if !viewModel.isRefreshing {
|
||||
tableView.refreshControl?.endRefreshing()
|
||||
@ -371,6 +357,10 @@ private final class MaterialStatsCell: UITableViewCell {
|
||||
contentView.backgroundColor = AppColor.pageBackground
|
||||
cardView.backgroundColor = .white
|
||||
cardView.layer.cornerRadius = 12
|
||||
cardView.layer.shadowColor = UIColor.black.cgColor
|
||||
cardView.layer.shadowOpacity = 0.12
|
||||
cardView.layer.shadowRadius = 2
|
||||
cardView.layer.shadowOffset = CGSize(width: 0, height: 1)
|
||||
stack.axis = .horizontal
|
||||
stack.spacing = 12
|
||||
stack.distribution = .fillEqually
|
||||
@ -451,7 +441,7 @@ private final class MaterialListCell: UITableViewCell {
|
||||
private let statusBadge = MaterialStatusBadgeView()
|
||||
private let listingSwitch = UISwitch()
|
||||
private let statsStack = UIStackView()
|
||||
private let downloadStatView = MaterialStatView(systemIconName: "arrow.down.circle")
|
||||
private let downloadStatView = MaterialStatView(iconName: "material_ic_download_count")
|
||||
private let likeStatView = MaterialStatView(iconName: "sample_ic_like_count")
|
||||
private let collectStatView = MaterialStatView(iconName: "sample_ic_collect_count")
|
||||
private var isApplying = false
|
||||
|
||||
258
suixinkan/UI/MaterialManagement/MaterialManagementDialogs.swift
Normal file
258
suixinkan/UI/MaterialManagement/MaterialManagementDialogs.swift
Normal file
@ -0,0 +1,258 @@
|
||||
//
|
||||
// MaterialManagementDialogs.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import AVKit
|
||||
import Kingfisher
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 素材模块 Android 风格红色删除确认弹窗。
|
||||
final class MaterialDeleteDialogViewController: UIViewController {
|
||||
private let dialogTitle: String
|
||||
private let itemName: String
|
||||
private let itemSuffix: String
|
||||
private let onConfirm: () -> Void
|
||||
private let onCancel: () -> Void
|
||||
private let dimView = UIView()
|
||||
private let cardView = UIView()
|
||||
|
||||
/// 创建素材删除确认弹窗。
|
||||
init(
|
||||
title: String = "删除素材",
|
||||
itemName: String,
|
||||
itemSuffix: String = "素材吗?",
|
||||
onCancel: @escaping () -> Void = {},
|
||||
onConfirm: @escaping () -> Void
|
||||
) {
|
||||
dialogTitle = title
|
||||
self.itemName = itemName
|
||||
self.itemSuffix = itemSuffix
|
||||
self.onConfirm = onConfirm
|
||||
self.onCancel = onCancel
|
||||
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 = .clear
|
||||
dimView.backgroundColor = UIColor.black.withAlphaComponent(0.32)
|
||||
cardView.backgroundColor = .white
|
||||
cardView.layer.cornerRadius = 16
|
||||
|
||||
let stack = UIStackView()
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 16
|
||||
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = dialogTitle
|
||||
titleLabel.font = .systemFont(ofSize: 18, weight: .bold)
|
||||
titleLabel.textColor = UIColor(hex: 0xFF0000)
|
||||
|
||||
let messageLabel = UILabel()
|
||||
messageLabel.numberOfLines = 0
|
||||
let prefix = "您确定要删除"
|
||||
let suffix = itemSuffix
|
||||
let message = NSMutableAttributedString(
|
||||
string: prefix + itemName + suffix,
|
||||
attributes: [
|
||||
.font: UIFont.systemFont(ofSize: 16),
|
||||
.foregroundColor: UIColor(hex: 0x333333),
|
||||
]
|
||||
)
|
||||
if !itemName.isEmpty {
|
||||
let prefixLength = (prefix as NSString).length
|
||||
let itemLength = (itemName as NSString).length
|
||||
message.addAttribute(
|
||||
.font,
|
||||
value: UIFont.systemFont(ofSize: 16, weight: .bold),
|
||||
range: NSRange(location: prefixLength, length: itemLength)
|
||||
)
|
||||
}
|
||||
messageLabel.attributedText = message
|
||||
|
||||
let buttons = UIStackView()
|
||||
buttons.axis = .horizontal
|
||||
buttons.spacing = 12
|
||||
buttons.distribution = .fillEqually
|
||||
let cancelButton = makeButton(title: "取消", titleColor: UIColor(hex: 0xFF0000), background: UIColor(hex: 0xFFE5E5))
|
||||
let confirmButton = makeButton(title: "确认", titleColor: .white, background: UIColor(hex: 0xFF0000))
|
||||
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
|
||||
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
|
||||
[cancelButton, confirmButton].forEach { button in
|
||||
button.snp.makeConstraints { make in make.height.equalTo(44) }
|
||||
buttons.addArrangedSubview(button)
|
||||
}
|
||||
|
||||
view.addSubview(dimView)
|
||||
view.addSubview(cardView)
|
||||
cardView.addSubview(stack)
|
||||
[titleLabel, messageLabel, buttons].forEach(stack.addArrangedSubview)
|
||||
stack.setCustomSpacing(24, after: messageLabel)
|
||||
dimView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
cardView.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.leading.trailing.equalToSuperview().inset(32)
|
||||
}
|
||||
stack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(24) }
|
||||
}
|
||||
|
||||
private func makeButton(title: String, titleColor: UIColor, background: UIColor) -> UIButton {
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitle(title, for: .normal)
|
||||
button.setTitleColor(titleColor, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
button.backgroundColor = background
|
||||
button.layer.cornerRadius = 8
|
||||
return button
|
||||
}
|
||||
|
||||
@objc private func cancelTapped() {
|
||||
dismiss(animated: true) { [onCancel] in onCancel() }
|
||||
}
|
||||
|
||||
@objc private func confirmTapped() {
|
||||
dismiss(animated: true) { [onConfirm] in onConfirm() }
|
||||
}
|
||||
}
|
||||
|
||||
/// 素材预览数据,统一描述图片和视频。
|
||||
struct MaterialPreviewItem: Equatable {
|
||||
let url: String
|
||||
let mediaType: MaterialMediaType
|
||||
}
|
||||
|
||||
/// Android `MediaPreviewDialog` 对齐的全屏素材预览页。
|
||||
final class MaterialMediaPreviewViewController: UIViewController, UIScrollViewDelegate {
|
||||
private let items: [MaterialPreviewItem]
|
||||
private let startIndex: Int
|
||||
private let scrollView = UIScrollView()
|
||||
private let closeButton = UIButton(type: .system)
|
||||
private var pages: [UIView] = []
|
||||
private var players: [Int: AVPlayerViewController] = [:]
|
||||
private var currentIndex = 0
|
||||
|
||||
/// 创建全屏素材预览页。
|
||||
init(items: [MaterialPreviewItem], startIndex: Int) {
|
||||
self.items = items
|
||||
self.startIndex = max(0, min(startIndex, max(0, items.count - 1)))
|
||||
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 = .black
|
||||
scrollView.backgroundColor = .black
|
||||
scrollView.isPagingEnabled = true
|
||||
scrollView.showsHorizontalScrollIndicator = false
|
||||
scrollView.bounces = false
|
||||
scrollView.delegate = self
|
||||
closeButton.setImage(UIImage(named: "material_ic_clear"), for: .normal)
|
||||
closeButton.tintColor = .white
|
||||
closeButton.backgroundColor = UIColor.black.withAlphaComponent(0.5)
|
||||
closeButton.layer.cornerRadius = 22
|
||||
closeButton.accessibilityLabel = "关闭"
|
||||
closeButton.addTarget(self, action: #selector(closeTapped), for: .touchUpInside)
|
||||
|
||||
view.addSubview(scrollView)
|
||||
view.addSubview(closeButton)
|
||||
scrollView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
closeButton.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide).offset(5)
|
||||
make.trailing.equalToSuperview().inset(16)
|
||||
make.size.equalTo(44)
|
||||
}
|
||||
buildPages()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
let size = scrollView.bounds.size
|
||||
guard size.width > 0, size.height > 0 else { return }
|
||||
for (index, page) in pages.enumerated() {
|
||||
page.frame = CGRect(x: CGFloat(index) * size.width, y: 0, width: size.width, height: size.height)
|
||||
}
|
||||
scrollView.contentSize = CGSize(width: CGFloat(items.count) * size.width, height: size.height)
|
||||
scrollView.contentOffset = CGPoint(x: CGFloat(currentIndex) * size.width, y: 0)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
currentIndex = startIndex
|
||||
view.setNeedsLayout()
|
||||
view.layoutIfNeeded()
|
||||
playCurrentVideo()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
players.values.forEach { $0.player?.pause() }
|
||||
}
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
updateCurrentIndex()
|
||||
}
|
||||
|
||||
private func buildPages() {
|
||||
for (index, item) in items.enumerated() {
|
||||
let page = UIView()
|
||||
page.backgroundColor = .black
|
||||
scrollView.addSubview(page)
|
||||
pages.append(page)
|
||||
if item.mediaType == .video, let url = URL(string: item.url) {
|
||||
let controller = AVPlayerViewController()
|
||||
controller.player = AVPlayer(url: url)
|
||||
controller.showsPlaybackControls = true
|
||||
addChild(controller)
|
||||
page.addSubview(controller.view)
|
||||
controller.view.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
controller.didMove(toParent: self)
|
||||
players[index] = controller
|
||||
} else {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.backgroundColor = .black
|
||||
page.addSubview(imageView)
|
||||
imageView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
if let url = URL(string: item.url) {
|
||||
imageView.kf.setImage(with: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateCurrentIndex() {
|
||||
guard scrollView.bounds.width > 0 else { return }
|
||||
currentIndex = max(0, min(Int(round(scrollView.contentOffset.x / scrollView.bounds.width)), items.count - 1))
|
||||
playCurrentVideo()
|
||||
}
|
||||
|
||||
private func playCurrentVideo() {
|
||||
players.forEach { index, controller in
|
||||
if index == currentIndex {
|
||||
controller.player?.play()
|
||||
} else {
|
||||
controller.player?.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func closeTapped() {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
@ -41,7 +41,7 @@ final class DepositOrderDetailViewController: BaseViewController, UITableViewDat
|
||||
}
|
||||
|
||||
private func loadDetail() async {
|
||||
let storeId = AppStore.shared.currentStoreId
|
||||
let storeId = AppStore.shared.session.currentStoreId
|
||||
guard storeId > 0 else {
|
||||
showToast("店铺信息异常")
|
||||
return
|
||||
|
||||
@ -18,7 +18,7 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
|
||||
private let listView = OrderListView()
|
||||
|
||||
private var hasInitialized = false
|
||||
private var isStoreMode: Bool { AppStore.shared.currentAppRole == .storeAdmin }
|
||||
private var isStoreMode: Bool { AppStore.shared.session.currentAppRole == .storeAdmin }
|
||||
private var pendingOrderSourceNumber: String?
|
||||
private weak var orderSourcePicker: OrderSourcePickerViewController?
|
||||
private var pendingGiftOrderNumber: String?
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
448
suixinkan/UI/Profile/ProfileSpaceSettingsDialogs.swift
Normal file
448
suixinkan/UI/Profile/ProfileSpaceSettingsDialogs.swift
Normal 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)) }
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -302,7 +302,7 @@ final class ScenicApplicationViewController: BaseViewController {
|
||||
}
|
||||
|
||||
private func startUploadIfNeeded() async {
|
||||
let scenicId = AppStore.shared.currentScenicId
|
||||
let scenicId = AppStore.shared.session.currentScenicId
|
||||
await viewModel.processPendingUploads(uploader: ossUploadService, scenicId: scenicId)
|
||||
}
|
||||
}
|
||||
|
||||
@ -307,7 +307,7 @@ final class TaskAddViewController: BaseViewController {
|
||||
}
|
||||
|
||||
private func uploadPickedResults(_ results: [PHPickerResult], isImage: Bool) {
|
||||
let scenicId = AppStore.shared.currentScenicId
|
||||
let scenicId = AppStore.shared.session.currentScenicId
|
||||
guard scenicId > 0 else {
|
||||
showToast("请先选择景区")
|
||||
return
|
||||
@ -344,7 +344,7 @@ final class TaskAddViewController: BaseViewController {
|
||||
}
|
||||
|
||||
private func uploadCapturedPayload(_ payload: TaskMediaLoader.Payload, fileType: Int) {
|
||||
let scenicId = AppStore.shared.currentScenicId
|
||||
let scenicId = AppStore.shared.session.currentScenicId
|
||||
guard scenicId > 0 else {
|
||||
showToast("请先选择景区")
|
||||
return
|
||||
|
||||
@ -33,12 +33,23 @@ final class TaskOrderSelectViewController: BaseViewController {
|
||||
title = "订单列表"
|
||||
}
|
||||
|
||||
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() {
|
||||
tableView.backgroundColor = AppColor.pageBackground
|
||||
tableView.separatorStyle = .none
|
||||
tableView.register(TaskOrderCell.self, forCellReuseIdentifier: TaskOrderCell.reuseIdentifier)
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.contentInset = UIEdgeInsets(top: 12, left: 0, bottom: 0, right: 0)
|
||||
|
||||
emptyLabel.text = "无有效订单"
|
||||
emptyLabel.font = .app(.body)
|
||||
@ -51,6 +62,7 @@ final class TaskOrderSelectViewController: BaseViewController {
|
||||
view.addSubview(emptyLabel)
|
||||
view.addSubview(bottomBar)
|
||||
bottomBar.addSubview(confirmButton)
|
||||
confirmButton.snp.updateConstraints { make in make.height.equalTo(48) }
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
@ -58,7 +70,9 @@ final class TaskOrderSelectViewController: BaseViewController {
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
confirmButton.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(AppSpacing.md)
|
||||
make.top.equalToSuperview().offset(16)
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(16)
|
||||
}
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
|
||||
@ -135,6 +149,8 @@ private final class TaskOrderCell: UITableViewCell {
|
||||
private let statusRow = TaskInfoRowView(label: "订单状态:")
|
||||
private let numberRow = TaskInfoRowView(label: "订单编号:")
|
||||
private let payTimeRow = TaskInfoRowView(label: "付款时间:")
|
||||
private let userIDRow = TaskInfoRowView(label: "用户 UID:")
|
||||
private let phoneRow = TaskInfoRowView(label: "用户手机号:")
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
@ -152,6 +168,8 @@ private final class TaskOrderCell: UITableViewCell {
|
||||
cardView.addSubview(statusRow)
|
||||
cardView.addSubview(numberRow)
|
||||
cardView.addSubview(payTimeRow)
|
||||
cardView.addSubview(userIDRow)
|
||||
cardView.addSubview(phoneRow)
|
||||
|
||||
cardView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: AppSpacing.md, bottom: AppSpacing.sm, right: AppSpacing.md))
|
||||
@ -169,6 +187,14 @@ private final class TaskOrderCell: UITableViewCell {
|
||||
}
|
||||
payTimeRow.snp.makeConstraints { make in
|
||||
make.top.equalTo(numberRow.snp.bottom).offset(AppSpacing.xs)
|
||||
make.leading.trailing.equalTo(projectRow)
|
||||
}
|
||||
userIDRow.snp.makeConstraints { make in
|
||||
make.top.equalTo(payTimeRow.snp.bottom).offset(AppSpacing.xs)
|
||||
make.leading.trailing.equalTo(projectRow)
|
||||
}
|
||||
phoneRow.snp.makeConstraints { make in
|
||||
make.top.equalTo(userIDRow.snp.bottom).offset(AppSpacing.xs)
|
||||
make.leading.trailing.bottom.equalToSuperview().inset(AppSpacing.md)
|
||||
}
|
||||
}
|
||||
@ -183,8 +209,12 @@ private final class TaskOrderCell: UITableViewCell {
|
||||
statusRow.apply(value: order.orderStatusLabel.isEmpty ? "--" : order.orderStatusLabel)
|
||||
numberRow.apply(value: order.orderNumber)
|
||||
payTimeRow.apply(value: order.payTime.isEmpty ? "--" : order.payTime)
|
||||
cardView.layer.borderColor = isSelected ? AppColor.danger.cgColor : AppColor.border.cgColor
|
||||
userIDRow.apply(value: "U\(order.userId)")
|
||||
phoneRow.apply(value: order.userPhone)
|
||||
cardView.layer.borderColor = isSelected ? UIColor(hex: 0xFF0000).cgColor : AppColor.border.cgColor
|
||||
cardView.layer.borderWidth = isSelected ? 2 : 1
|
||||
accessibilityLabel = "订单\(order.orderNumber)"
|
||||
accessibilityTraits = isSelected ? [.button, .selected] : .button
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,17 +227,15 @@ private final class TaskInfoRowView: UIView {
|
||||
super.init(frame: .zero)
|
||||
labelView.text = label
|
||||
labelView.font = .app(.body)
|
||||
labelView.textColor = AppColor.textSecondary
|
||||
labelView.textColor = AppColor.textPrimary
|
||||
valueView.font = .app(.body)
|
||||
valueView.textColor = AppColor.textPrimary
|
||||
valueView.numberOfLines = 0
|
||||
|
||||
addSubview(labelView)
|
||||
addSubview(valueView)
|
||||
labelView.snp.makeConstraints { make in
|
||||
make.leading.top.bottom.equalToSuperview()
|
||||
make.width.equalTo(80)
|
||||
}
|
||||
labelView.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
labelView.snp.makeConstraints { make in make.leading.top.bottom.equalToSuperview() }
|
||||
valueView.snp.makeConstraints { make in
|
||||
make.leading.equalTo(labelView.snp.trailing).offset(AppSpacing.xs)
|
||||
make.trailing.top.bottom.equalToSuperview()
|
||||
|
||||
@ -164,7 +164,7 @@ final class WildPhotographerReportListViewController: BaseViewController {
|
||||
}
|
||||
|
||||
private var currentScenicId: Int? {
|
||||
let scenicId = AppStore.shared.currentScenicId
|
||||
let scenicId = AppStore.shared.session.currentScenicId
|
||||
return scenicId > 0 ? scenicId : nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -556,8 +556,8 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
|
||||
await viewModel.submitReport(
|
||||
api: api,
|
||||
uploader: uploader,
|
||||
scenicId: AppStore.shared.currentScenicId,
|
||||
scenicName: AppStore.shared.currentScenicName
|
||||
scenicId: AppStore.shared.session.currentScenicId,
|
||||
scenicName: AppStore.shared.session.currentScenicName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,8 +34,8 @@ final class WildReportRiskMapViewController: BaseViewController {
|
||||
init(
|
||||
api: any WildPhotographerReportServing = NetworkServices.shared.wildPhotographerReportAPI,
|
||||
locationProvider: any LocationProviding = LocationProvider.shared,
|
||||
scenicIdProvider: @escaping () -> Int = { AppStore.shared.currentScenicId },
|
||||
scenicNameProvider: @escaping () -> String = { AppStore.shared.currentScenicName }
|
||||
scenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId },
|
||||
scenicNameProvider: @escaping () -> String = { AppStore.shared.session.currentScenicName }
|
||||
) {
|
||||
self.viewModel = WildReportRiskMapViewModel()
|
||||
self.api = api
|
||||
|
||||
@ -343,7 +343,7 @@ final class WildReportSupplementEvidenceViewController: BaseViewController {
|
||||
await viewModel.submit(
|
||||
api: api,
|
||||
uploader: uploader,
|
||||
scenicId: AppStore.shared.currentScenicId
|
||||
scenicId: AppStore.shared.session.currentScenicId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user