Initial commit: suixinkan_ios UIKit rewrite project.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,360 @@
|
||||
//
|
||||
// DepositOrderViewControllers.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 押金订单列表页,支持查询、分页、核销和退款。
|
||||
final class DepositOrderListViewController: UIViewController {
|
||||
|
||||
private let viewModel = DepositOrderListViewModel()
|
||||
private var orderNumberField = UITextField()
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView(frame: .zero, style: .insetGrouped)
|
||||
table.dataSource = self
|
||||
table.delegate = self
|
||||
table.register(DepositOrderCell.self, forCellReuseIdentifier: DepositOrderCell.reuseID)
|
||||
return table
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "押金订单"
|
||||
view.backgroundColor = AppDesignUIKit.pageBackground
|
||||
view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
|
||||
viewModel.onChange = { [weak self] in self?.tableView.reloadData() }
|
||||
setupHeader()
|
||||
Task { await reload(showLoading: true) }
|
||||
}
|
||||
|
||||
private func setupHeader() {
|
||||
let header = UIView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 120))
|
||||
orderNumberField.placeholder = "请输入押金订单号"
|
||||
orderNumberField.borderStyle = .roundedRect
|
||||
orderNumberField.autocorrectionType = .no
|
||||
orderNumberField.autocapitalizationType = .none
|
||||
|
||||
let detailButton = makePrimaryButton(title: "查看订单详情")
|
||||
detailButton.addTarget(self, action: #selector(openDetail), for: .touchUpInside)
|
||||
|
||||
header.addSubview(orderNumberField)
|
||||
header.addSubview(detailButton)
|
||||
orderNumberField.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview().inset(16)
|
||||
make.height.equalTo(44)
|
||||
}
|
||||
detailButton.snp.makeConstraints { make in
|
||||
make.top.equalTo(orderNumberField.snp.bottom).offset(12)
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
}
|
||||
tableView.tableHeaderView = header
|
||||
}
|
||||
|
||||
@objc private func openDetail() {
|
||||
let text = orderNumberField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard !text.isEmpty else { return }
|
||||
HomeMenuRouting.pushOrders(.depositDetail(orderNumber: text), from: self)
|
||||
}
|
||||
|
||||
private func reload(showLoading: Bool) async {
|
||||
if showLoading {
|
||||
await appServices.globalLoading.withLoading {
|
||||
await viewModel.reload(api: appServices.ordersAPI, scenicId: appServices.accountContext.currentScenic?.id, reset: true)
|
||||
}
|
||||
} else {
|
||||
await viewModel.reload(api: appServices.ordersAPI, scenicId: appServices.accountContext.currentScenic?.id, reset: true)
|
||||
}
|
||||
if let message = viewModel.errorMessage { showToast(message) }
|
||||
}
|
||||
|
||||
private func writeOff(_ item: DepositOrderListItem) {
|
||||
let alert = UIAlertController(title: "确认核销该押金订单?", message: item.orderNumber, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "确认核销", style: .default) { [weak self] _ in
|
||||
Task {
|
||||
guard let self else { return }
|
||||
let success = await self.viewModel.writeOff(api: self.appServices.ordersAPI, scenicId: self.appServices.accountContext.currentScenic?.id, orderNumber: item.orderNumber)
|
||||
if success { self.showToast("核销成功") }
|
||||
else if let message = self.viewModel.errorMessage { self.showToast(message) }
|
||||
}
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func refund(_ item: DepositOrderListItem) {
|
||||
let alert = UIAlertController(title: "申请退款", message: "请填写退款原因", preferredStyle: .alert)
|
||||
alert.addTextField { $0.placeholder = "退款原因" }
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "提交", style: .default) { [weak self] _ in
|
||||
Task {
|
||||
guard let self else { return }
|
||||
let reason = alert.textFields?.first?.text ?? ""
|
||||
let success = await self.viewModel.refund(api: self.appServices.ordersAPI, scenicId: self.appServices.accountContext.currentScenic?.id, orderNumber: item.orderNumber, reason: reason)
|
||||
if success { self.showToast("退款申请已提交") }
|
||||
else if let message = self.viewModel.errorMessage { self.showToast(message) }
|
||||
}
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension DepositOrderListViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
max(viewModel.orders.count, 1)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
"押金订单列表 \(viewModel.orders.count)/\(viewModel.total)"
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
guard !viewModel.orders.isEmpty else {
|
||||
let cell = UITableViewCell()
|
||||
cell.textLabel?.text = viewModel.loading ? "加载中..." : "暂无押金订单"
|
||||
cell.selectionStyle = .none
|
||||
return cell
|
||||
}
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: DepositOrderCell.reuseID, for: indexPath) as! DepositOrderCell
|
||||
let item = viewModel.orders[indexPath.row]
|
||||
cell.configure(item: item, isOperating: viewModel.operatingOrderNumber == item.orderNumber)
|
||||
cell.onDetail = { [weak self] in
|
||||
self.flatMap { HomeMenuRouting.pushOrders(.depositDetail(orderNumber: item.orderNumber), from: $0) }
|
||||
}
|
||||
cell.onWriteOff = { [weak self] in self?.writeOff(item) }
|
||||
cell.onRefund = { [weak self] in self?.refund(item) }
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
guard indexPath.row == viewModel.orders.count - 1, viewModel.hasMore else { return }
|
||||
Task {
|
||||
await viewModel.reload(api: appServices.ordersAPI, scenicId: appServices.accountContext.currentScenic?.id, reset: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class DepositOrderCell: UITableViewCell {
|
||||
static let reuseID = "DepositOrderCell"
|
||||
var onDetail: (() -> Void)?
|
||||
var onWriteOff: (() -> Void)?
|
||||
var onRefund: (() -> Void)?
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
private let statusLabel = UILabel()
|
||||
private let amountLabel = UILabel()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline, weight: .semibold)
|
||||
statusLabel.font = .systemFont(ofSize: AppMetrics.FontSize.caption)
|
||||
amountLabel.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .semibold)
|
||||
amountLabel.textColor = AppDesignUIKit.primary
|
||||
|
||||
let detailButton = UIButton(type: .system)
|
||||
detailButton.setTitle("详情", for: .normal)
|
||||
detailButton.addTarget(self, action: #selector(detailTapped), for: .touchUpInside)
|
||||
let writeOffButton = UIButton(type: .system)
|
||||
writeOffButton.setTitle("核销", for: .normal)
|
||||
writeOffButton.addTarget(self, action: #selector(writeOffTapped), for: .touchUpInside)
|
||||
let refundButton = UIButton(type: .system)
|
||||
refundButton.setTitle("退款", for: .normal)
|
||||
refundButton.addTarget(self, action: #selector(refundTapped), for: .touchUpInside)
|
||||
|
||||
let buttonRow = UIStackView(arrangedSubviews: [detailButton, writeOffButton, refundButton])
|
||||
buttonRow.axis = .horizontal
|
||||
buttonRow.distribution = .fillEqually
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [titleLabel, amountLabel, buttonRow])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 8
|
||||
contentView.addSubview(stack)
|
||||
contentView.addSubview(statusLabel)
|
||||
stack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(12) }
|
||||
statusLabel.snp.makeConstraints { make in make.trailing.top.equalToSuperview().inset(12) }
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
func configure(item: DepositOrderListItem, isOperating: Bool) {
|
||||
titleLabel.text = item.orderNumber
|
||||
statusLabel.text = isOperating ? "处理中" : item.statusName
|
||||
amountLabel.text = "¥\(item.amount.isEmpty ? "0.00" : item.amount)"
|
||||
}
|
||||
|
||||
@objc private func detailTapped() { onDetail?() }
|
||||
@objc private func writeOffTapped() { onWriteOff?() }
|
||||
@objc private func refundTapped() { onRefund?() }
|
||||
}
|
||||
|
||||
/// 押金订单详情页。
|
||||
final class DepositOrderDetailViewController: UIViewController {
|
||||
|
||||
private let orderNumber: String
|
||||
private let viewModel = DepositOrderDetailViewModel()
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView(frame: .zero, style: .insetGrouped)
|
||||
table.dataSource = self
|
||||
table.backgroundColor = AppDesignUIKit.pageBackground
|
||||
return table
|
||||
}()
|
||||
|
||||
init(orderNumber: String) {
|
||||
self.orderNumber = orderNumber
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "押金订单详情"
|
||||
view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
Task { await loadDetail() }
|
||||
}
|
||||
|
||||
private func loadDetail() async {
|
||||
await appServices.globalLoading.withLoading {
|
||||
await viewModel.load(
|
||||
api: appServices.ordersAPI,
|
||||
storeId: appServices.accountContext.currentStore?.id,
|
||||
orderNumber: orderNumber
|
||||
)
|
||||
}
|
||||
tableView.reloadData()
|
||||
if let message = viewModel.errorMessage { showToast(message) }
|
||||
}
|
||||
}
|
||||
|
||||
extension DepositOrderDetailViewController: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
viewModel.detail == nil ? 1 : 8
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
|
||||
cell.selectionStyle = .none
|
||||
guard let detail = viewModel.detail else {
|
||||
cell.textLabel?.text = viewModel.errorMessage ?? (viewModel.loading ? "加载中..." : "暂无详情")
|
||||
return cell
|
||||
}
|
||||
let rows: [(String, String)] = [
|
||||
("订单号", detail.orderNumber),
|
||||
("状态", detail.orderStatusName),
|
||||
("类型", detail.orderTypeLabel),
|
||||
("付款金额", detail.actualPayAmount),
|
||||
("手机号", detail.phone),
|
||||
("项目", detail.projectName),
|
||||
("创建时间", detail.createdAt),
|
||||
("备注", detail.remark)
|
||||
]
|
||||
cell.textLabel?.text = rows[indexPath.row].0
|
||||
cell.detailTextLabel?.text = rows[indexPath.row].1
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
/// 押金拍摄信息页。
|
||||
final class DepositOrderShootingInfoViewController: UIViewController {
|
||||
|
||||
private let orderNumber: String
|
||||
private let scenicSpotId: Int
|
||||
private let photogUid: Int
|
||||
private let viewModel = DepositOrderShootingInfoViewModel()
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView(frame: .zero, style: .insetGrouped)
|
||||
table.dataSource = self
|
||||
return table
|
||||
}()
|
||||
|
||||
init(orderNumber: String, scenicSpotId: Int, photogUid: Int) {
|
||||
self.orderNumber = orderNumber
|
||||
self.scenicSpotId = scenicSpotId
|
||||
self.photogUid = photogUid
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "押金拍摄信息"
|
||||
view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
Task { await loadDetail() }
|
||||
}
|
||||
|
||||
private func loadDetail() async {
|
||||
await appServices.globalLoading.withLoading {
|
||||
await viewModel.load(
|
||||
api: appServices.ordersAPI,
|
||||
storeId: appServices.accountContext.currentStore?.id,
|
||||
orderNumber: orderNumber,
|
||||
scenicSpotId: scenicSpotId,
|
||||
photogUid: photogUid
|
||||
)
|
||||
}
|
||||
tableView.reloadData()
|
||||
if let message = viewModel.errorMessage { showToast(message) }
|
||||
}
|
||||
}
|
||||
|
||||
extension DepositOrderShootingInfoViewController: UITableViewDataSource {
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
viewModel.detail == nil ? 1 : 3
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
guard let detail = viewModel.detail else { return 1 }
|
||||
switch section {
|
||||
case 0: return 2
|
||||
case 1: return detail.materialList.count
|
||||
default: return detail.completeList.count
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
guard viewModel.detail != nil else { return nil }
|
||||
switch section {
|
||||
case 1: return "素材"
|
||||
case 2: return "成片"
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
guard let detail = viewModel.detail else {
|
||||
let cell = UITableViewCell()
|
||||
cell.textLabel?.text = viewModel.errorMessage ?? "加载中..."
|
||||
return cell
|
||||
}
|
||||
if indexPath.section == 0 {
|
||||
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
|
||||
if indexPath.row == 0 {
|
||||
cell.textLabel?.text = "打卡点"
|
||||
cell.detailTextLabel?.text = detail.scenicSpotName
|
||||
} else {
|
||||
cell.textLabel?.text = "拍摄评分"
|
||||
cell.detailTextLabel?.text = detail.orderComment.map { "\($0.starShooting) 星" } ?? "--"
|
||||
}
|
||||
return cell
|
||||
}
|
||||
let files = indexPath.section == 1 ? detail.materialList : detail.completeList
|
||||
let media = files[indexPath.row]
|
||||
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
|
||||
cell.textLabel?.text = media.fileName
|
||||
cell.detailTextLabel?.text = media.fileUrl
|
||||
return cell
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,301 @@
|
||||
//
|
||||
// OrderCodeScannerViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 订单扫码错误实体,描述相机不可用、权限拒绝和元数据异常。
|
||||
enum OrderScannerError: LocalizedError {
|
||||
case cameraUnavailable
|
||||
case permissionDenied
|
||||
case invalidMetadata
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .cameraUnavailable:
|
||||
"当前设备不可用相机,无法扫码。"
|
||||
case .permissionDenied:
|
||||
"未开启相机权限,请在系统设置中允许后重试。"
|
||||
case .invalidMetadata:
|
||||
"未识别到有效条码,请重试。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 订单扫码页,包装 AVFoundation 扫码控制器并提供关闭与结果回调。
|
||||
final class OrderCodeScannerViewController: UIViewController {
|
||||
|
||||
var onScanResult: ((Result<String, Error>) -> Void)?
|
||||
|
||||
private let scannerController = OrderScannerCaptureViewController()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .black
|
||||
title = "扫码核销"
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "关闭", style: .plain, target: self, action: #selector(closeTapped))
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
showUnavailablePlaceholder()
|
||||
#else
|
||||
if AppUITestLaunchState.isRunningUITests || AVCaptureDevice.default(for: .video) == nil {
|
||||
showUnavailablePlaceholder()
|
||||
} else {
|
||||
embedScanner()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func embedScanner() {
|
||||
scannerController.onScanResult = { [weak self] result in
|
||||
self?.onScanResult?(result)
|
||||
}
|
||||
addChild(scannerController)
|
||||
view.addSubview(scannerController.view)
|
||||
scannerController.view.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
scannerController.didMove(toParent: self)
|
||||
|
||||
let hintLabel = UILabel()
|
||||
hintLabel.text = "请将二维码/条码放入取景框"
|
||||
hintLabel.font = .systemFont(ofSize: AppMetrics.FontSize.footnote, weight: .medium)
|
||||
hintLabel.textColor = .white
|
||||
hintLabel.backgroundColor = UIColor.black.withAlphaComponent(0.55)
|
||||
hintLabel.layer.cornerRadius = 16
|
||||
hintLabel.clipsToBounds = true
|
||||
hintLabel.textAlignment = .center
|
||||
view.addSubview(hintLabel)
|
||||
hintLabel.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(AppMetrics.Spacing.xxLarge)
|
||||
make.height.equalTo(32)
|
||||
make.width.greaterThanOrEqualTo(220)
|
||||
}
|
||||
}
|
||||
|
||||
private func showUnavailablePlaceholder() {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .vertical
|
||||
stack.spacing = AppMetrics.Spacing.small
|
||||
stack.alignment = .center
|
||||
|
||||
let icon = UIImageView(image: UIImage(systemName: "camera.viewfinder"))
|
||||
icon.tintColor = .white
|
||||
icon.contentMode = .scaleAspectFit
|
||||
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = "当前环境不可用相机"
|
||||
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.title3, weight: .semibold)
|
||||
titleLabel.textColor = .white
|
||||
|
||||
let messageLabel = UILabel()
|
||||
messageLabel.text = "请在真机上使用扫码核销"
|
||||
messageLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
|
||||
messageLabel.textColor = UIColor.white.withAlphaComponent(0.72)
|
||||
|
||||
stack.addArrangedSubview(icon)
|
||||
stack.addArrangedSubview(titleLabel)
|
||||
stack.addArrangedSubview(messageLabel)
|
||||
view.addSubview(stack)
|
||||
icon.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(48)
|
||||
}
|
||||
stack.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func closeTapped() {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
/// AVFoundation 扫码控制器,管理相机权限、预览层、识别类型和手电筒。
|
||||
final class OrderScannerCaptureViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
|
||||
|
||||
var onScanResult: ((Result<String, Error>) -> Void)?
|
||||
|
||||
private let session = AVCaptureSession()
|
||||
private var previewLayer: AVCaptureVideoPreviewLayer?
|
||||
private var videoDevice: AVCaptureDevice?
|
||||
private var hasFinishedScan = false
|
||||
|
||||
private let scanFrameView: UIView = {
|
||||
let view = UIView()
|
||||
view.layer.borderColor = UIColor.systemBlue.cgColor
|
||||
view.layer.borderWidth = 2
|
||||
view.layer.cornerRadius = 12
|
||||
view.backgroundColor = .clear
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var torchButton: UIButton = {
|
||||
let button = Self.makeScannerButton(title: "手电筒")
|
||||
button.addTarget(self, action: #selector(toggleTorch), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
private lazy var retryButton: UIButton = {
|
||||
let button = Self.makeScannerButton(title: "重新扫描")
|
||||
button.addTarget(self, action: #selector(restartScan), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .black
|
||||
setupControls()
|
||||
checkPermissionAndStart()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
previewLayer?.frame = view.layer.bounds
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
updateTorch(enabled: false)
|
||||
stopSession()
|
||||
}
|
||||
|
||||
private func checkPermissionAndStart() {
|
||||
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
||||
case .authorized:
|
||||
startSession()
|
||||
case .notDetermined:
|
||||
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
|
||||
DispatchQueue.main.async {
|
||||
guard let self else { return }
|
||||
if granted {
|
||||
self.startSession()
|
||||
} else {
|
||||
self.onScanResult?(.failure(OrderScannerError.permissionDenied))
|
||||
}
|
||||
}
|
||||
}
|
||||
case .denied, .restricted:
|
||||
onScanResult?(.failure(OrderScannerError.permissionDenied))
|
||||
@unknown default:
|
||||
onScanResult?(.failure(OrderScannerError.permissionDenied))
|
||||
}
|
||||
}
|
||||
|
||||
private func startSession() {
|
||||
hasFinishedScan = false
|
||||
guard !session.isRunning else { return }
|
||||
do {
|
||||
try configureSession()
|
||||
} catch {
|
||||
onScanResult?(.failure(error))
|
||||
return
|
||||
}
|
||||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
||||
self?.session.startRunning()
|
||||
}
|
||||
}
|
||||
|
||||
private func stopSession() {
|
||||
guard session.isRunning else { return }
|
||||
session.stopRunning()
|
||||
}
|
||||
|
||||
private static func makeScannerButton(title: String) -> UIButton {
|
||||
var config = UIButton.Configuration.filled()
|
||||
config.title = title
|
||||
config.baseForegroundColor = .white
|
||||
config.baseBackgroundColor = UIColor.black.withAlphaComponent(0.42)
|
||||
config.cornerStyle = .capsule
|
||||
return UIButton(configuration: config)
|
||||
}
|
||||
|
||||
private func configureSession() throws {
|
||||
if previewLayer != nil { return }
|
||||
guard let videoDevice = AVCaptureDevice.default(for: .video) else {
|
||||
throw OrderScannerError.cameraUnavailable
|
||||
}
|
||||
self.videoDevice = videoDevice
|
||||
|
||||
let videoInput = try AVCaptureDeviceInput(device: videoDevice)
|
||||
guard session.canAddInput(videoInput) else { throw OrderScannerError.cameraUnavailable }
|
||||
session.addInput(videoInput)
|
||||
|
||||
let metadataOutput = AVCaptureMetadataOutput()
|
||||
guard session.canAddOutput(metadataOutput) else { throw OrderScannerError.invalidMetadata }
|
||||
session.addOutput(metadataOutput)
|
||||
|
||||
metadataOutput.setMetadataObjectsDelegate(self, queue: .main)
|
||||
let desiredTypes: [AVMetadataObject.ObjectType] = [.qr, .ean8, .ean13, .pdf417, .code39, .code93, .code128, .dataMatrix, .aztec, .itf14, .upce]
|
||||
let supportedTypes = desiredTypes.filter { metadataOutput.availableMetadataObjectTypes.contains($0) }
|
||||
guard !supportedTypes.isEmpty else { throw OrderScannerError.invalidMetadata }
|
||||
metadataOutput.metadataObjectTypes = supportedTypes
|
||||
|
||||
let preview = AVCaptureVideoPreviewLayer(session: session)
|
||||
preview.videoGravity = .resizeAspectFill
|
||||
preview.frame = view.layer.bounds
|
||||
view.layer.insertSublayer(preview, at: 0)
|
||||
previewLayer = preview
|
||||
torchButton.isEnabled = videoDevice.hasTorch
|
||||
}
|
||||
|
||||
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
|
||||
guard !hasFinishedScan else { return }
|
||||
guard let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
|
||||
let code = metadataObject.stringValue,
|
||||
!code.isEmpty else { return }
|
||||
hasFinishedScan = true
|
||||
stopSession()
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||
onScanResult?(.success(code))
|
||||
}
|
||||
|
||||
private func setupControls() {
|
||||
view.addSubview(scanFrameView)
|
||||
view.addSubview(torchButton)
|
||||
view.addSubview(retryButton)
|
||||
|
||||
scanFrameView.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.centerY.equalToSuperview().offset(-40)
|
||||
make.width.height.equalTo(240)
|
||||
}
|
||||
torchButton.snp.makeConstraints { make in
|
||||
make.leading.equalTo(view.safeAreaLayoutGuide).inset(16)
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(18)
|
||||
}
|
||||
retryButton.snp.makeConstraints { make in
|
||||
make.trailing.equalTo(view.safeAreaLayoutGuide).inset(16)
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(18)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func toggleTorch() {
|
||||
updateTorch(enabled: !(videoDevice?.isTorchActive ?? false))
|
||||
}
|
||||
|
||||
private func updateTorch(enabled: Bool) {
|
||||
guard let device = videoDevice, device.hasTorch else { return }
|
||||
do {
|
||||
try device.lockForConfiguration()
|
||||
if enabled {
|
||||
try device.setTorchModeOn(level: AVCaptureDevice.maxAvailableTorchLevel)
|
||||
} else {
|
||||
device.torchMode = .off
|
||||
}
|
||||
device.unlockForConfiguration()
|
||||
torchButton.configuration?.title = enabled ? "关闭手电" : "手电筒"
|
||||
} catch {
|
||||
torchButton.configuration?.title = "手电筒不可用"
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func restartScan() {
|
||||
hasFinishedScan = false
|
||||
updateTorch(enabled: false)
|
||||
startSession()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,252 @@
|
||||
//
|
||||
// OrderDetailViewControllers.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 门店订单详情页,展示订单接口补全后的支付、客户、项目和拍摄点信息。
|
||||
final class StoreOrderDetailViewController: UIViewController {
|
||||
|
||||
private let item: OrderEntity
|
||||
private let viewModel: OrderDetailViewModel
|
||||
private let refundViewModel = OrderRefundViewModel()
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView(frame: .zero, style: .insetGrouped)
|
||||
table.dataSource = self
|
||||
table.delegate = self
|
||||
table.backgroundColor = AppDesignUIKit.pageBackground
|
||||
return table
|
||||
}()
|
||||
|
||||
init(item: OrderEntity) {
|
||||
self.item = item
|
||||
self.viewModel = OrderDetailViewModel(item: item)
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "订单详情"
|
||||
view.backgroundColor = AppDesignUIKit.pageBackground
|
||||
view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
|
||||
viewModel.onChange = { [weak self] in self?.tableView.reloadData() }
|
||||
Task { await loadDetail() }
|
||||
}
|
||||
|
||||
private func loadDetail() async {
|
||||
await appServices.globalLoading.withLoading(message: "加载详情中...") {
|
||||
await viewModel.load(api: appServices.ordersAPI, fallbackStoreId: appServices.accountContext.currentStore?.id)
|
||||
}
|
||||
if let error = viewModel.errorMessage {
|
||||
showToast(error)
|
||||
}
|
||||
}
|
||||
|
||||
private func copyOrderNumber() {
|
||||
UIPasteboard.general.string = viewModel.display.orderNumber
|
||||
showToast("订单号已复制")
|
||||
}
|
||||
|
||||
private func presentRefund() {
|
||||
refundViewModel.begin(item: item)
|
||||
let alert = UIAlertController(title: "订单退款", message: "可退 ¥\(refundViewModel.availableAmountText(for: item))", preferredStyle: .alert)
|
||||
alert.addTextField { $0.placeholder = "退款原因" }
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "提交", style: .default) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.refundViewModel.reason = alert.textFields?.first?.text ?? ""
|
||||
Task {
|
||||
let success = await self.refundViewModel.submit(api: self.appServices.ordersAPI, item: self.item)
|
||||
if success {
|
||||
self.showToast("退款申请已提交")
|
||||
await self.viewModel.load(api: self.appServices.ordersAPI, fallbackStoreId: self.appServices.accountContext.currentStore?.id, forceReload: true)
|
||||
} else if let message = self.refundViewModel.errorMessage {
|
||||
self.showToast(message)
|
||||
}
|
||||
}
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension StoreOrderDetailViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
func numberOfSections(in tableView: UITableView) -> Int { 6 }
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch section {
|
||||
case 0: return viewModel.contextMessage == nil ? 0 : 1
|
||||
case 1: return 7
|
||||
case 2: return 4
|
||||
case 3: return 4
|
||||
case 4: return viewModel.shootingList.count
|
||||
case 5: return 5
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch section {
|
||||
case 1: "订单信息"
|
||||
case 2: "支付信息"
|
||||
case 3: "客户与项目"
|
||||
case 4 where !viewModel.shootingList.isEmpty: "拍摄点"
|
||||
case 5: "后续功能"
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
|
||||
cell.selectionStyle = .none
|
||||
let display = viewModel.display
|
||||
|
||||
switch indexPath.section {
|
||||
case 0:
|
||||
cell.textLabel?.text = viewModel.contextMessage
|
||||
cell.textLabel?.numberOfLines = 0
|
||||
case 1:
|
||||
let rows = ["订单号", "状态", "类型", "创建时间", "付款时间", "完成时间", "复制订单号"]
|
||||
cell.textLabel?.text = rows[indexPath.row]
|
||||
switch indexPath.row {
|
||||
case 0: cell.detailTextLabel?.text = display.orderNumber
|
||||
case 1: cell.detailTextLabel?.text = display.orderStatusName
|
||||
case 2: cell.detailTextLabel?.text = display.orderTypeLabel
|
||||
case 3: cell.detailTextLabel?.text = display.createdAt
|
||||
case 4: cell.detailTextLabel?.text = displayPayTime
|
||||
case 5: cell.detailTextLabel?.text = display.completeTime
|
||||
default:
|
||||
cell.textLabel?.textColor = AppDesignUIKit.primary
|
||||
cell.selectionStyle = .default
|
||||
}
|
||||
case 2:
|
||||
let rows = ["付款金额", "退款金额", "付款方式", "用户 UID"]
|
||||
cell.textLabel?.text = rows[indexPath.row]
|
||||
switch indexPath.row {
|
||||
case 0: cell.detailTextLabel?.text = "¥\(emptyToZero(display.actualPayAmount))"
|
||||
case 1: cell.detailTextLabel?.text = "¥\(emptyToZero(display.actualRefundAmount))"
|
||||
case 2: cell.detailTextLabel?.text = display.payTypeName
|
||||
default: cell.detailTextLabel?.text = "\(display.userId)"
|
||||
}
|
||||
case 3:
|
||||
let rows = ["手机号", "关联项目", "项目 ID", "备注"]
|
||||
cell.textLabel?.text = rows[indexPath.row]
|
||||
switch indexPath.row {
|
||||
case 0: cell.detailTextLabel?.text = display.phone
|
||||
case 1: cell.detailTextLabel?.text = display.projectName
|
||||
case 2: cell.detailTextLabel?.text = "\(display.projectId)"
|
||||
default: cell.detailTextLabel?.text = display.remark
|
||||
}
|
||||
case 4:
|
||||
let shooting = viewModel.shootingList[indexPath.row]
|
||||
cell.textLabel?.text = shooting.scenicSpotName
|
||||
cell.detailTextLabel?.text = shooting.staffName.isEmpty ? "状态 \(shooting.status)" : shooting.staffName
|
||||
case 5:
|
||||
let actions = ["历史拍摄", "任务上传", "视频预告", "尾片上传", "退款"]
|
||||
cell.textLabel?.text = actions[indexPath.row]
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
cell.selectionStyle = .default
|
||||
if indexPath.row == 4, !refundViewModel.canRefund(item) {
|
||||
cell.isUserInteractionEnabled = false
|
||||
cell.textLabel?.textColor = AppDesignUIKit.textSecondary
|
||||
}
|
||||
default: break
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
if indexPath.section == 1, indexPath.row == 6 { copyOrderNumber(); return }
|
||||
guard indexPath.section == 5 else { return }
|
||||
let orderNumber = viewModel.display.orderNumber
|
||||
switch indexPath.row {
|
||||
case 0:
|
||||
HomeMenuRouting.pushOrders(.historicalShooting(orderNumber: orderNumber), from: self)
|
||||
case 1 where item.orderType == 19:
|
||||
HomeMenuRouting.pushOrders(.multiTravelTaskUpload(orderNumber: orderNumber), from: self)
|
||||
case 2:
|
||||
HomeMenuRouting.pushOrders(.orderTrailer(orderNumber: orderNumber, title: "视频预告"), from: self)
|
||||
case 3:
|
||||
HomeMenuRouting.pushOrders(.orderTrailer(orderNumber: orderNumber, title: "尾片上传"), from: self)
|
||||
case 4:
|
||||
presentRefund()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
private var displayPayTime: String {
|
||||
let payTime = viewModel.display.payTime
|
||||
if payTime.isEmpty || payTime == "0" || payTime.hasPrefix("1970") { return item.depositPayTime }
|
||||
return payTime
|
||||
}
|
||||
|
||||
private func emptyToZero(_ value: String) -> String {
|
||||
value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "0" : value
|
||||
}
|
||||
}
|
||||
|
||||
/// 核销订单详情页,展示核销列表项摘要信息。
|
||||
final class WriteOffOrderDetailViewController: UIViewController {
|
||||
|
||||
private let item: WriteOffOrderItem
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView(frame: .zero, style: .insetGrouped)
|
||||
table.dataSource = self
|
||||
table.backgroundColor = AppDesignUIKit.pageBackground
|
||||
return table
|
||||
}()
|
||||
|
||||
init(item: WriteOffOrderItem) {
|
||||
self.item = item
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "核销详情"
|
||||
view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
}
|
||||
}
|
||||
|
||||
extension WriteOffOrderDetailViewController: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 6 }
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
|
||||
cell.selectionStyle = .none
|
||||
switch indexPath.row {
|
||||
case 0:
|
||||
cell.textLabel?.text = "订单号"
|
||||
cell.detailTextLabel?.text = item.orderNumber
|
||||
case 1:
|
||||
cell.textLabel?.text = "项目"
|
||||
cell.detailTextLabel?.text = item.projectName
|
||||
case 2:
|
||||
cell.textLabel?.text = "手机号"
|
||||
cell.detailTextLabel?.text = item.userPhone
|
||||
case 3:
|
||||
cell.textLabel?.text = "金额"
|
||||
cell.detailTextLabel?.text = "¥\(item.orderAmount)"
|
||||
case 4:
|
||||
cell.textLabel?.text = "状态"
|
||||
cell.detailTextLabel?.text = item.orderStatusName
|
||||
default:
|
||||
cell.textLabel?.text = "核销时间"
|
||||
cell.detailTextLabel?.text = item.orderVerificationTime
|
||||
}
|
||||
return cell
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,268 @@
|
||||
//
|
||||
// OrderTailUploadViewControllers.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import PhotosUI
|
||||
import SnapKit
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
/// 多点旅拍任务上传页,负责选择打卡点和素材后提交到订单接口。
|
||||
final class MultiTravelTaskUploadViewController: UIViewController {
|
||||
|
||||
private let viewModel: MultiTravelTaskUploadViewModel
|
||||
|
||||
private let orderField = UITextField()
|
||||
private let spotButton = UIButton(type: .system)
|
||||
private let cloudListLabel = UILabel()
|
||||
private let localListLabel = UILabel()
|
||||
private let submitButton = UIButton(type: .system)
|
||||
|
||||
init(initialOrderNumber: String) {
|
||||
self.viewModel = MultiTravelTaskUploadViewModel(initialOrderNumber: initialOrderNumber)
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "任务上传"
|
||||
view.backgroundColor = UIColor(hex: 0xF5F7FA)
|
||||
setupForm()
|
||||
bindViewModel()
|
||||
Task { await viewModel.loadSpots(api: appServices.ordersAPI) }
|
||||
}
|
||||
|
||||
private func bindViewModel() {
|
||||
viewModel.onChange = { [weak self] in
|
||||
self?.applyViewModel()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupForm() {
|
||||
orderField.borderStyle = .roundedRect
|
||||
orderField.placeholder = "关联订单号"
|
||||
orderField.autocorrectionType = .no
|
||||
orderField.autocapitalizationType = .none
|
||||
orderField.text = viewModel.orderNumber
|
||||
orderField.addTarget(self, action: #selector(orderChanged), for: .editingChanged)
|
||||
|
||||
spotButton.setTitle("选择打卡点", for: .normal)
|
||||
spotButton.addTarget(self, action: #selector(selectSpot), for: .touchUpInside)
|
||||
|
||||
cloudListLabel.numberOfLines = 0
|
||||
cloudListLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
|
||||
localListLabel.numberOfLines = 0
|
||||
localListLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
|
||||
|
||||
submitButton.configuration = .filled()
|
||||
submitButton.configuration?.title = "保存任务素材"
|
||||
submitButton.configuration?.baseBackgroundColor = AppDesignUIKit.primary
|
||||
submitButton.addTarget(self, action: #selector(submitTapped), for: .touchUpInside)
|
||||
|
||||
let refreshButton = UIButton(type: .system)
|
||||
refreshButton.setTitle("刷新打卡点", for: .normal)
|
||||
refreshButton.addTarget(self, action: #selector(refreshSpots), for: .touchUpInside)
|
||||
|
||||
let pickLocalButton = UIButton(type: .system)
|
||||
pickLocalButton.setTitle("选择图片/视频", for: .normal)
|
||||
pickLocalButton.addTarget(self, action: #selector(pickLocalFiles), for: .touchUpInside)
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [
|
||||
labeledRow("订单号", orderField),
|
||||
refreshButton,
|
||||
spotButton,
|
||||
labeledRow("云盘素材", cloudListLabel),
|
||||
pickLocalButton,
|
||||
labeledRow("本地素材", localListLabel),
|
||||
submitButton
|
||||
])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 12
|
||||
view.addSubview(stack)
|
||||
stack.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide).offset(16)
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
}
|
||||
applyViewModel()
|
||||
}
|
||||
|
||||
private func labeledRow(_ title: String, _ content: UIView) -> UIStackView {
|
||||
let label = UILabel()
|
||||
label.text = title
|
||||
label.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .semibold)
|
||||
let row = UIStackView(arrangedSubviews: [label, content])
|
||||
row.axis = .vertical
|
||||
row.spacing = 8
|
||||
return row
|
||||
}
|
||||
|
||||
private func applyViewModel() {
|
||||
orderField.text = viewModel.orderNumber
|
||||
spotButton.setTitle(viewModel.isLoadingSpots ? "加载中..." : viewModel.selectedSpotName, for: .normal)
|
||||
spotButton.isEnabled = !viewModel.isLoadingSpots
|
||||
|
||||
if viewModel.selectedCloudFiles.isEmpty {
|
||||
cloudListLabel.text = "未选择云盘文件"
|
||||
} else {
|
||||
cloudListLabel.text = viewModel.selectedCloudFiles.map(\.fileName).joined(separator: "\n")
|
||||
}
|
||||
|
||||
if viewModel.selectedLocalFiles.isEmpty {
|
||||
localListLabel.text = "未选择本地文件"
|
||||
} else {
|
||||
localListLabel.text = viewModel.selectedLocalFiles.map { file in
|
||||
"\(file.fileName) \(file.statusText)"
|
||||
}.joined(separator: "\n")
|
||||
}
|
||||
|
||||
submitButton.isEnabled = viewModel.canSubmit
|
||||
submitButton.configuration?.title = viewModel.isSubmitting ? "保存中..." : "保存任务素材"
|
||||
}
|
||||
|
||||
@objc private func orderChanged() {
|
||||
viewModel.orderNumber = orderField.text ?? ""
|
||||
}
|
||||
|
||||
@objc private func refreshSpots() {
|
||||
Task { await viewModel.loadSpots(api: appServices.ordersAPI) }
|
||||
}
|
||||
|
||||
@objc private func selectSpot() {
|
||||
guard !viewModel.spots.isEmpty else { return }
|
||||
let sheet = UIAlertController(title: "选择打卡点", message: nil, preferredStyle: .actionSheet)
|
||||
for spot in viewModel.spots {
|
||||
let title = spot.name.isEmpty ? "打卡点 \(spot.id)" : spot.name
|
||||
sheet.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
|
||||
self?.viewModel.selectSpot(id: spot.id)
|
||||
})
|
||||
}
|
||||
sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
present(sheet, animated: true)
|
||||
}
|
||||
|
||||
@objc private func pickLocalFiles() {
|
||||
var config = PHPickerConfiguration()
|
||||
config.selectionLimit = 9
|
||||
config.filter = .any(of: [.images, .videos])
|
||||
let picker = PHPickerViewController(configuration: config)
|
||||
picker.delegate = self
|
||||
present(picker, animated: true)
|
||||
}
|
||||
|
||||
@objc private func submitTapped() {
|
||||
Task {
|
||||
let success = await viewModel.submit(
|
||||
api: appServices.ordersAPI,
|
||||
uploadService: appServices.ossUploadService,
|
||||
scenicId: appServices.accountContext.currentScenic?.id
|
||||
)
|
||||
if success {
|
||||
showToast("提交成功")
|
||||
navigationController?.popViewController(animated: true)
|
||||
} else if let message = viewModel.errorMessage {
|
||||
showToast(message)
|
||||
}
|
||||
applyViewModel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MultiTravelTaskUploadViewController: PHPickerViewControllerDelegate {
|
||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
picker.dismiss(animated: true)
|
||||
guard !results.isEmpty else { return }
|
||||
for result in results {
|
||||
let provider = result.itemProvider
|
||||
if provider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
|
||||
provider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { [weak self] url, _ in
|
||||
guard let self, let url else { return }
|
||||
let data = (try? Data(contentsOf: url)) ?? Data()
|
||||
DispatchQueue.main.async {
|
||||
self.viewModel.addLocalFile(data: data, fileName: url.lastPathComponent)
|
||||
self.applyViewModel()
|
||||
}
|
||||
}
|
||||
} else if provider.canLoadObject(ofClass: UIImage.self) {
|
||||
provider.loadObject(ofClass: UIImage.self) { [weak self] object, _ in
|
||||
guard let self, let image = object as? UIImage, let data = image.jpegData(compressionQuality: 0.9) else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.viewModel.addLocalFile(data: data, fileName: "upload_\(Int(Date().timeIntervalSince1970)).jpg")
|
||||
self.applyViewModel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 历史拍摄信息页。
|
||||
final class HistoricalShootingInfoViewController: UIViewController {
|
||||
|
||||
private let orderNumber: String
|
||||
private let viewModel = HistoricalShootingInfoViewModel()
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView(frame: .zero, style: .insetGrouped)
|
||||
table.dataSource = self
|
||||
table.delegate = self
|
||||
return table
|
||||
}()
|
||||
|
||||
init(orderNumber: String) {
|
||||
self.orderNumber = orderNumber
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "历史拍摄"
|
||||
view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
viewModel.onChange = { [weak self] in self?.tableView.reloadData() }
|
||||
Task { await loadData() }
|
||||
}
|
||||
|
||||
private func loadData() async {
|
||||
await appServices.globalLoading.withLoading {
|
||||
await viewModel.load(api: appServices.ordersAPI, orderNumber: orderNumber)
|
||||
}
|
||||
if let message = viewModel.errorMessage { showToast(message) }
|
||||
}
|
||||
}
|
||||
|
||||
extension HistoricalShootingInfoViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
func numberOfSections(in tableView: UITableView) -> Int { 2 }
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
section == 0 ? 2 : max(viewModel.spots.count, 1)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
section == 1 ? "拍摄点位" : nil
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
|
||||
cell.selectionStyle = .none
|
||||
if indexPath.section == 0 {
|
||||
cell.textLabel?.text = indexPath.row == 0 ? "项目" : "项目类型"
|
||||
cell.detailTextLabel?.text = indexPath.row == 0 ? viewModel.projectName : viewModel.projectTypeName
|
||||
return cell
|
||||
}
|
||||
guard !viewModel.spots.isEmpty else {
|
||||
cell.textLabel?.text = viewModel.errorMessage ?? "暂无历史拍摄"
|
||||
return cell
|
||||
}
|
||||
let spot = viewModel.spots[indexPath.row]
|
||||
cell.textLabel?.text = spot.scenicSpotName
|
||||
cell.detailTextLabel?.text = "\(spot.files.count) 个文件 · \(spot.photographerDisplayName)"
|
||||
return cell
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,604 @@
|
||||
//
|
||||
// OrdersViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 订单 Tab 根页面,展示订单管理和核销订单两个子入口及对应列表。
|
||||
final class OrdersViewController: UIViewController {
|
||||
|
||||
private let viewModel = OrdersViewModel()
|
||||
private var manualOrderNumber = ""
|
||||
private var scanHintMessage: String?
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView(frame: .zero, style: .grouped)
|
||||
table.backgroundColor = AppDesignUIKit.pageBackground
|
||||
table.separatorStyle = .none
|
||||
table.dataSource = self
|
||||
table.delegate = self
|
||||
table.register(OrderEntityCell.self, forCellReuseIdentifier: OrderEntityCell.reuseID)
|
||||
table.register(WriteOffOrderCell.self, forCellReuseIdentifier: WriteOffOrderCell.reuseID)
|
||||
table.register(OrdersHeaderCell.self, forCellReuseIdentifier: OrdersHeaderCell.reuseID)
|
||||
table.register(OrdersFilterCell.self, forCellReuseIdentifier: OrdersFilterCell.reuseID)
|
||||
table.register(OrdersWriteOffActionCell.self, forCellReuseIdentifier: OrdersWriteOffActionCell.reuseID)
|
||||
return table
|
||||
}()
|
||||
|
||||
private lazy var refreshControl = UIRefreshControl()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "订单"
|
||||
view.backgroundColor = AppDesignUIKit.pageBackground
|
||||
|
||||
view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
refreshControl.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
|
||||
tableView.refreshControl = refreshControl
|
||||
|
||||
viewModel.onChange = { [weak self] in
|
||||
self?.tableView.reloadData()
|
||||
}
|
||||
|
||||
appServices.appRouter.onChange = { [weak self] in
|
||||
guard let self else { return }
|
||||
if self.viewModel.selectedEntry != self.appServices.appRouter.selectedOrdersEntry {
|
||||
self.viewModel.selectedEntry = self.appServices.appRouter.selectedOrdersEntry
|
||||
Task { await self.reload(showLoading: true) }
|
||||
}
|
||||
Task { await self.consumePendingScanCodeIfNeeded() }
|
||||
}
|
||||
appServices.accountContext.onChange = { [weak self] in
|
||||
Task { await self?.reload(showLoading: true) }
|
||||
}
|
||||
|
||||
viewModel.selectedEntry = appServices.appRouter.selectedOrdersEntry
|
||||
Task {
|
||||
await reload(showLoading: true)
|
||||
await consumePendingScanCodeIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func refreshPulled() {
|
||||
Task {
|
||||
await reload(showLoading: false)
|
||||
refreshControl.endRefreshing()
|
||||
}
|
||||
}
|
||||
|
||||
private var currentScenicId: Int? { appServices.accountContext.currentScenic?.id }
|
||||
private var currentStoreId: Int? { appServices.accountContext.currentStore?.id }
|
||||
private var currentRoleId: Int? { appServices.permissionContext.currentRole?.id }
|
||||
|
||||
private func reload(showLoading: Bool) async {
|
||||
do {
|
||||
try await appServices.globalLoading.withOptionalLoading(showLoading, message: "加载订单...") {
|
||||
try await self.viewModel.reload(
|
||||
api: self.appServices.ordersAPI,
|
||||
scenicId: self.currentScenicId,
|
||||
storeId: self.currentStoreId,
|
||||
roleId: self.currentRoleId,
|
||||
showLoading: false
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadMore() async {
|
||||
do {
|
||||
if viewModel.selectedEntry == .storeOrders {
|
||||
try await viewModel.loadMoreStoreOrders(
|
||||
api: appServices.ordersAPI,
|
||||
scenicId: currentScenicId,
|
||||
roleId: currentRoleId
|
||||
)
|
||||
} else {
|
||||
try await viewModel.loadMoreWriteOffOrders(
|
||||
api: appServices.ordersAPI,
|
||||
scenicId: currentScenicId,
|
||||
storeId: currentStoreId
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func switchEntry(_ entry: OrdersEntry) {
|
||||
guard viewModel.selectedEntry != entry else { return }
|
||||
viewModel.selectedEntry = entry
|
||||
appServices.appRouter.selectedOrdersEntry = entry
|
||||
Task { await reload(showLoading: true) }
|
||||
}
|
||||
|
||||
private func verify(orderNumber: String) async {
|
||||
guard let scenicId = currentScenicId else { return }
|
||||
do {
|
||||
try await appServices.globalLoading.withLoading(message: "核销中...") {
|
||||
try await self.viewModel.verify(
|
||||
api: self.appServices.ordersAPI,
|
||||
scenicId: scenicId,
|
||||
storeId: self.currentStoreId,
|
||||
orderNumber: orderNumber
|
||||
)
|
||||
}
|
||||
showToast("核销成功")
|
||||
} catch {
|
||||
showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func consumePendingScanCodeIfNeeded() async {
|
||||
guard viewModel.selectedEntry == .verificationOrders,
|
||||
let code = appServices.appRouter.consumePendingOrderScanCode() else { return }
|
||||
handleScanResult(code)
|
||||
}
|
||||
|
||||
private func handleScanResult(_ rawCode: String) {
|
||||
guard let parsed = viewModel.matchedWriteOffOrder(for: rawCode) else {
|
||||
showToast("未识别到有效订单号")
|
||||
return
|
||||
}
|
||||
if parsed.matched != nil {
|
||||
confirmVerify(orderNumber: parsed.orderNumber)
|
||||
} else {
|
||||
scanHintMessage = "扫码成功,当前列表未找到该订单"
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
private func confirmVerify(orderNumber: String) {
|
||||
let alert = UIAlertController(title: "确认核销该订单?", message: orderNumber, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "确认核销", style: .default) { [weak self] _ in
|
||||
Task { await self?.verify(orderNumber: orderNumber) }
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func presentScanner() {
|
||||
let scanner = OrderCodeScannerViewController()
|
||||
scanner.onScanResult = { [weak self, weak scanner] result in
|
||||
scanner?.dismiss(animated: true)
|
||||
guard let self else { return }
|
||||
switch result {
|
||||
case .success(let code):
|
||||
self.handleScanResult(code)
|
||||
case .failure(let error):
|
||||
self.showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
let nav = UINavigationController(rootViewController: scanner)
|
||||
nav.modalPresentationStyle = .fullScreen
|
||||
present(nav, animated: true)
|
||||
}
|
||||
|
||||
private func presentDateFilter() {
|
||||
let alert = UIAlertController(title: "时间筛选", message: "选择开始和结束日期", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "清除筛选", style: .destructive) { [weak self] _ in
|
||||
self?.viewModel.filterStartDate = nil
|
||||
self?.viewModel.filterEndDate = nil
|
||||
Task { await self?.reload(showLoading: true) }
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "近7天", style: .default) { [weak self] _ in
|
||||
let end = Date()
|
||||
let start = Calendar.current.date(byAdding: .day, value: -6, to: end) ?? end
|
||||
self?.viewModel.filterStartDate = start
|
||||
self?.viewModel.filterEndDate = end
|
||||
Task { await self?.reload(showLoading: true) }
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func presentStatusFilter() {
|
||||
let sheet = UIAlertController(title: "订单状态", message: nil, preferredStyle: .actionSheet)
|
||||
for filter in OrderFilters.statusFilters {
|
||||
sheet.addAction(UIAlertAction(title: filter.title, style: .default) { [weak self] _ in
|
||||
self?.viewModel.selectedStatus = filter.id
|
||||
Task { await self?.reload(showLoading: true) }
|
||||
})
|
||||
}
|
||||
sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
present(sheet, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension OrdersViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
guard currentScenicId != nil else { return 1 }
|
||||
return viewModel.selectedEntry == .storeOrders ? 3 : 3
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
if section == 0 { return 1 }
|
||||
if section == 1 { return viewModel.selectedEntry == .storeOrders ? 1 : 1 }
|
||||
if currentScenicId == nil { return 1 }
|
||||
if viewModel.selectedEntry == .storeOrders {
|
||||
return max(viewModel.storeOrders.count, viewModel.loading && viewModel.storeOrders.isEmpty ? 0 : 1)
|
||||
}
|
||||
return max(viewModel.writeOffOrders.count, viewModel.loading && viewModel.writeOffOrders.isEmpty ? 0 : 1)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
if indexPath.section == 0 {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: OrdersHeaderCell.reuseID, for: indexPath) as! OrdersHeaderCell
|
||||
let services = appServices
|
||||
cell.configure(
|
||||
selectedEntry: viewModel.selectedEntry,
|
||||
scenicName: services.accountContext.currentScenic?.name ?? "--",
|
||||
storeTotal: viewModel.storeTotal,
|
||||
writeOffTotal: viewModel.writeOffTotal,
|
||||
onSelectEntry: { [weak self] entry in self?.switchEntry(entry) }
|
||||
)
|
||||
return cell
|
||||
}
|
||||
|
||||
if currentScenicId == nil {
|
||||
let cell = UITableViewCell()
|
||||
cell.selectionStyle = .none
|
||||
cell.backgroundColor = .clear
|
||||
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
let empty = makeEmptyStateView(title: "缺少经营上下文", message: "请先在首页选择景区后查看订单。", systemImage: "mountain.2")
|
||||
cell.contentView.addSubview(empty)
|
||||
empty.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
make.height.equalTo(360)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
if indexPath.section == 1 {
|
||||
if viewModel.selectedEntry == .storeOrders {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: OrdersFilterCell.reuseID, for: indexPath) as! OrdersFilterCell
|
||||
let statusTitle = OrderFilters.statusFilters.first(where: { $0.id == viewModel.selectedStatus })?.title ?? "全部"
|
||||
cell.configure(
|
||||
statusTitle: statusTitle,
|
||||
phone: viewModel.searchPhone,
|
||||
onStatus: { [weak self] in self?.presentStatusFilter() },
|
||||
onDate: { [weak self] in self?.presentDateFilter() },
|
||||
onPhoneChange: { [weak self] text in self?.viewModel.searchPhone = text },
|
||||
onSearch: { [weak self] in Task { await self?.reload(showLoading: true) } }
|
||||
)
|
||||
return cell
|
||||
}
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: OrdersWriteOffActionCell.reuseID, for: indexPath) as! OrdersWriteOffActionCell
|
||||
cell.configure(
|
||||
manualOrderNumber: manualOrderNumber,
|
||||
isVerifying: viewModel.isVerifying,
|
||||
hint: scanHintMessage,
|
||||
onScan: { [weak self] in self?.presentScanner() },
|
||||
onManualChange: { [weak self] text in self?.manualOrderNumber = text },
|
||||
onVerify: { [weak self] in
|
||||
guard let self, !self.manualOrderNumber.isEmpty else { return }
|
||||
self.confirmVerify(orderNumber: self.manualOrderNumber)
|
||||
}
|
||||
)
|
||||
return cell
|
||||
}
|
||||
|
||||
if viewModel.selectedEntry == .storeOrders {
|
||||
if viewModel.storeOrders.isEmpty {
|
||||
let cell = UITableViewCell()
|
||||
cell.selectionStyle = .none
|
||||
cell.backgroundColor = .clear
|
||||
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
let empty = makeEmptyStateView(title: "暂无订单", message: "可切换筛选条件或下拉刷新。", systemImage: "tray")
|
||||
cell.contentView.addSubview(empty)
|
||||
empty.snp.makeConstraints { make in make.edges.equalToSuperview(); make.height.equalTo(260) }
|
||||
return cell
|
||||
}
|
||||
let item = viewModel.storeOrders[indexPath.row]
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: OrderEntityCell.reuseID, for: indexPath) as! OrderEntityCell
|
||||
cell.configure(item: item)
|
||||
return cell
|
||||
}
|
||||
|
||||
if viewModel.writeOffOrders.isEmpty {
|
||||
let cell = UITableViewCell()
|
||||
cell.selectionStyle = .none
|
||||
cell.backgroundColor = .clear
|
||||
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
let empty = makeEmptyStateView(title: "暂无核销订单", message: "可下拉刷新或切换景区查看。", systemImage: "tray")
|
||||
cell.contentView.addSubview(empty)
|
||||
empty.snp.makeConstraints { make in make.edges.equalToSuperview(); make.height.equalTo(260) }
|
||||
return cell
|
||||
}
|
||||
let item = viewModel.writeOffOrders[indexPath.row]
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: WriteOffOrderCell.reuseID, for: indexPath) as! WriteOffOrderCell
|
||||
cell.configure(item: item, isVerifying: viewModel.currentVerifyingOrderNumber == item.orderNumber)
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
guard indexPath.section == 2 else { return }
|
||||
if viewModel.selectedEntry == .storeOrders, indexPath.row < viewModel.storeOrders.count {
|
||||
HomeMenuRouting.pushOrders(.storeDetail(viewModel.storeOrders[indexPath.row]), from: self)
|
||||
} else if viewModel.selectedEntry == .verificationOrders, indexPath.row < viewModel.writeOffOrders.count {
|
||||
HomeMenuRouting.pushOrders(.writeOffDetail(viewModel.writeOffOrders[indexPath.row]), from: self)
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
guard indexPath.section == 2 else { return }
|
||||
let isLast: Bool
|
||||
if viewModel.selectedEntry == .storeOrders {
|
||||
isLast = indexPath.row == viewModel.storeOrders.count - 1
|
||||
} else {
|
||||
isLast = indexPath.row == viewModel.writeOffOrders.count - 1
|
||||
}
|
||||
if isLast { Task { await loadMore() } }
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
if indexPath.section == 0 { return 120 }
|
||||
if indexPath.section == 1 { return viewModel.selectedEntry == .storeOrders ? 130 : 150 }
|
||||
return UITableView.automaticDimension
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cells
|
||||
|
||||
private final class OrdersHeaderCell: UITableViewCell {
|
||||
static let reuseID = "OrdersHeaderCell"
|
||||
private var onSelectEntry: ((OrdersEntry) -> Void)?
|
||||
|
||||
private let storeButton = UIButton(type: .system)
|
||||
private let verifyButton = UIButton(type: .system)
|
||||
private let leftPill = UILabel()
|
||||
private let rightPill = UILabel()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
let card = UIView()
|
||||
card.backgroundColor = .white
|
||||
card.layer.cornerRadius = 8
|
||||
contentView.addSubview(card)
|
||||
card.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 16, bottom: 4, right: 16))
|
||||
}
|
||||
|
||||
let segmentBackground = UIView()
|
||||
segmentBackground.backgroundColor = UIColor(hex: 0xF3F4F6)
|
||||
segmentBackground.layer.cornerRadius = 8
|
||||
card.addSubview(segmentBackground)
|
||||
storeButton.addTarget(self, action: #selector(storeTapped), for: .touchUpInside)
|
||||
verifyButton.addTarget(self, action: #selector(verifyTapped), for: .touchUpInside)
|
||||
segmentBackground.addSubview(storeButton)
|
||||
segmentBackground.addSubview(verifyButton)
|
||||
|
||||
[leftPill, rightPill].forEach {
|
||||
$0.font = .systemFont(ofSize: AppMetrics.FontSize.caption)
|
||||
$0.numberOfLines = 2
|
||||
card.addSubview($0)
|
||||
}
|
||||
|
||||
segmentBackground.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview().inset(12)
|
||||
make.height.equalTo(40)
|
||||
}
|
||||
storeButton.snp.makeConstraints { make in
|
||||
make.leading.top.bottom.equalToSuperview().inset(2)
|
||||
make.width.equalToSuperview().multipliedBy(0.5)
|
||||
}
|
||||
verifyButton.snp.makeConstraints { make in
|
||||
make.trailing.top.bottom.equalToSuperview().inset(2)
|
||||
make.width.equalToSuperview().multipliedBy(0.5)
|
||||
}
|
||||
leftPill.snp.makeConstraints { make in
|
||||
make.leading.bottom.equalToSuperview().inset(12)
|
||||
make.top.equalTo(segmentBackground.snp.bottom).offset(10)
|
||||
make.width.equalToSuperview().multipliedBy(0.45)
|
||||
}
|
||||
rightPill.snp.makeConstraints { make in
|
||||
make.trailing.bottom.equalToSuperview().inset(12)
|
||||
make.top.equalTo(leftPill)
|
||||
make.width.equalToSuperview().multipliedBy(0.45)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
func configure(selectedEntry: OrdersEntry, scenicName: String, storeTotal: Int, writeOffTotal: Int, onSelectEntry: @escaping (OrdersEntry) -> Void) {
|
||||
self.onSelectEntry = onSelectEntry
|
||||
updateSegment(storeButton, title: "订单管理", selected: selectedEntry == .storeOrders)
|
||||
updateSegment(verifyButton, title: "核销订单", selected: selectedEntry == .verificationOrders)
|
||||
if selectedEntry == .storeOrders {
|
||||
leftPill.text = "当前景区\n\(scenicName)"
|
||||
rightPill.text = "订单总数\n\(storeTotal)"
|
||||
} else {
|
||||
leftPill.text = "核销订单\n\(writeOffTotal)"
|
||||
rightPill.text = "当前景区\n\(scenicName)"
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSegment(_ button: UIButton, title: String, selected: Bool) {
|
||||
button.setTitle(title, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline, weight: selected ? .semibold : .medium)
|
||||
button.setTitleColor(selected ? AppDesignUIKit.primary : AppDesignUIKit.textSecondary, for: .normal)
|
||||
button.backgroundColor = selected ? .white : .clear
|
||||
button.layer.cornerRadius = 6
|
||||
}
|
||||
|
||||
@objc private func storeTapped() { onSelectEntry?(.storeOrders) }
|
||||
@objc private func verifyTapped() { onSelectEntry?(.verificationOrders) }
|
||||
}
|
||||
|
||||
private final class OrdersFilterCell: UITableViewCell, UITextFieldDelegate {
|
||||
static let reuseID = "OrdersFilterCell"
|
||||
private let phoneField = UITextField()
|
||||
private var onPhoneChange: ((String) -> Void)?
|
||||
private var onSearch: (() -> Void)?
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
let card = UIView()
|
||||
card.backgroundColor = .white
|
||||
card.layer.cornerRadius = 8
|
||||
contentView.addSubview(card)
|
||||
card.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 16, bottom: 4, right: 16))
|
||||
}
|
||||
phoneField.delegate = self
|
||||
phoneField.keyboardType = .phonePad
|
||||
phoneField.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
|
||||
phoneField.backgroundColor = UIColor(hex: 0xF4F4F4)
|
||||
phoneField.layer.cornerRadius = 6
|
||||
phoneField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 1))
|
||||
phoneField.leftViewMode = .always
|
||||
card.addSubview(phoneField)
|
||||
phoneField.snp.makeConstraints { make in
|
||||
make.leading.bottom.equalToSuperview().inset(12)
|
||||
make.height.equalTo(40)
|
||||
make.trailing.equalToSuperview().inset(100)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
func configure(statusTitle: String, phone: String, onStatus: @escaping () -> Void, onDate: @escaping () -> Void, onPhoneChange: @escaping (String) -> Void, onSearch: @escaping () -> Void) {
|
||||
phoneField.text = phone
|
||||
self.onPhoneChange = onPhoneChange
|
||||
self.onSearch = onSearch
|
||||
}
|
||||
|
||||
func textFieldDidChangeSelection(_ textField: UITextField) {
|
||||
onPhoneChange?(textField.text ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
private final class OrdersWriteOffActionCell: UITableViewCell, UITextFieldDelegate {
|
||||
static let reuseID = "OrdersWriteOffActionCell"
|
||||
private let manualField = UITextField()
|
||||
private var onManualChange: ((String) -> Void)?
|
||||
private var onVerify: (() -> Void)?
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
let card = UIView()
|
||||
card.backgroundColor = .white
|
||||
card.layer.cornerRadius = 8
|
||||
contentView.addSubview(card)
|
||||
card.snp.makeConstraints { make in make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 16, bottom: 4, right: 16)) }
|
||||
|
||||
manualField.delegate = self
|
||||
manualField.placeholder = "手动输入订单号"
|
||||
manualField.autocorrectionType = .no
|
||||
manualField.autocapitalizationType = .none
|
||||
manualField.backgroundColor = UIColor(hex: 0xF4F4F4)
|
||||
manualField.layer.cornerRadius = 8
|
||||
manualField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 1))
|
||||
manualField.leftViewMode = .always
|
||||
card.addSubview(manualField)
|
||||
manualField.snp.makeConstraints { make in
|
||||
make.leading.bottom.equalToSuperview().inset(12)
|
||||
make.height.equalTo(42)
|
||||
make.trailing.equalToSuperview().inset(90)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
func configure(manualOrderNumber: String, isVerifying: Bool, hint: String?, onScan: @escaping () -> Void, onManualChange: @escaping (String) -> Void, onVerify: @escaping () -> Void) {
|
||||
manualField.text = manualOrderNumber
|
||||
self.onManualChange = onManualChange
|
||||
self.onVerify = onVerify
|
||||
}
|
||||
|
||||
func textFieldDidChangeSelection(_ textField: UITextField) {
|
||||
onManualChange?(textField.text ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
private final class OrderEntityCell: UITableViewCell {
|
||||
static let reuseID = "OrderEntityCell"
|
||||
private let titleLabel = UILabel()
|
||||
private let statusLabel = UILabel()
|
||||
private let amountLabel = UILabel()
|
||||
private let phoneLabel = UILabel()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .default
|
||||
backgroundColor = .clear
|
||||
let card = UIView()
|
||||
card.backgroundColor = .white
|
||||
card.layer.cornerRadius = 8
|
||||
contentView.addSubview(card)
|
||||
card.snp.makeConstraints { make in make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 16, bottom: 4, right: 16)) }
|
||||
|
||||
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline, weight: .semibold)
|
||||
statusLabel.font = .systemFont(ofSize: AppMetrics.FontSize.caption, weight: .medium)
|
||||
amountLabel.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .semibold)
|
||||
amountLabel.textColor = AppDesignUIKit.primary
|
||||
phoneLabel.font = .systemFont(ofSize: AppMetrics.FontSize.caption)
|
||||
phoneLabel.textColor = AppDesignUIKit.textSecondary
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [titleLabel, phoneLabel, amountLabel])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 4
|
||||
card.addSubview(stack)
|
||||
card.addSubview(statusLabel)
|
||||
stack.snp.makeConstraints { make in make.leading.top.bottom.equalToSuperview().inset(12) }
|
||||
statusLabel.snp.makeConstraints { make in make.trailing.top.equalToSuperview().inset(12) }
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
func configure(item: OrderEntity) {
|
||||
titleLabel.text = item.orderNumber
|
||||
statusLabel.text = item.orderStatusName
|
||||
amountLabel.text = "¥\(item.actualPayAmount.isEmpty ? item.orderAmount : item.actualPayAmount)"
|
||||
phoneLabel.text = item.phone
|
||||
}
|
||||
}
|
||||
|
||||
private final class WriteOffOrderCell: UITableViewCell {
|
||||
static let reuseID = "WriteOffOrderCell"
|
||||
private let titleLabel = UILabel()
|
||||
private let subtitleLabel = UILabel()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
backgroundColor = .clear
|
||||
let card = UIView()
|
||||
card.backgroundColor = .white
|
||||
card.layer.cornerRadius = 8
|
||||
contentView.addSubview(card)
|
||||
card.snp.makeConstraints { make in make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 16, bottom: 4, right: 16)) }
|
||||
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline, weight: .semibold)
|
||||
subtitleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.caption)
|
||||
subtitleLabel.textColor = AppDesignUIKit.textSecondary
|
||||
let stack = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 4
|
||||
card.addSubview(stack)
|
||||
stack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(12) }
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
func configure(item: WriteOffOrderItem, isVerifying: Bool) {
|
||||
titleLabel.text = item.orderNumber
|
||||
subtitleLabel.text = isVerifying ? "核销中..." : (item.projectName.isEmpty ? item.userPhone : item.projectName)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user