Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
621 lines
22 KiB
Swift
621 lines
22 KiB
Swift
//
|
||
// OrderLongTailViewModels.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/25.
|
||
//
|
||
|
||
import Foundation
|
||
|
||
@MainActor
|
||
/// 押金订单列表 ViewModel,负责分页、核销、退款和操作后刷新。
|
||
final class DepositOrderListViewModel {
|
||
var onChange: (() -> Void)?
|
||
private(set) var orders: [DepositOrderListItem] = [] { didSet { onChange?() } }
|
||
private(set) var total = 0 { didSet { onChange?() } }
|
||
private(set) var page = 1 { didSet { onChange?() } }
|
||
private(set) var hasMore = false { didSet { onChange?() } }
|
||
private(set) var loading = false { didSet { onChange?() } }
|
||
private(set) var loadingMore = false { didSet { onChange?() } }
|
||
private(set) var operatingOrderNumber: String? { didSet { onChange?() } }
|
||
var errorMessage: String? { didSet { onChange?() } }
|
||
|
||
private let pageSize = 10
|
||
|
||
/// 加载押金订单列表,缺少景区时清空旧数据且不请求接口。
|
||
func reload(api: OrderServing, scenicId: Int?, reset: Bool) async {
|
||
guard let scenicId, scenicId > 0 else {
|
||
resetState()
|
||
return
|
||
}
|
||
|
||
if reset {
|
||
page = 1
|
||
total = 0
|
||
orders = []
|
||
loading = true
|
||
} else {
|
||
guard !loadingMore, hasMore else { return }
|
||
loadingMore = true
|
||
}
|
||
|
||
defer {
|
||
loading = false
|
||
loadingMore = false
|
||
}
|
||
|
||
do {
|
||
let result = try await api.depositOrderList(scenicId: scenicId, page: page, pageSize: pageSize)
|
||
total = result.total
|
||
if page == 1 {
|
||
orders = result.list
|
||
} else {
|
||
orders.append(contentsOf: result.list)
|
||
}
|
||
page += 1
|
||
hasMore = orders.count < result.total && !result.list.isEmpty
|
||
errorMessage = nil
|
||
} catch {
|
||
if reset {
|
||
orders = []
|
||
total = 0
|
||
page = 1
|
||
hasMore = false
|
||
}
|
||
errorMessage = error.localizedDescription
|
||
}
|
||
}
|
||
|
||
/// 核销押金订单,成功后刷新第一页。
|
||
func writeOff(api: OrderServing, scenicId: Int?, orderNumber: String) async -> Bool {
|
||
let normalized = orderNumber.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !normalized.isEmpty, operatingOrderNumber == nil else { return false }
|
||
guard let scenicId, scenicId > 0 else {
|
||
errorMessage = "缺少景区上下文"
|
||
return false
|
||
}
|
||
|
||
operatingOrderNumber = normalized
|
||
defer { operatingOrderNumber = nil }
|
||
|
||
do {
|
||
try await api.depositOrderWriteOff(orderNumber: normalized)
|
||
await reload(api: api, scenicId: scenicId, reset: true)
|
||
return true
|
||
} catch {
|
||
errorMessage = error.localizedDescription
|
||
return false
|
||
}
|
||
}
|
||
|
||
/// 申请押金订单退款,原因必填,成功后刷新第一页。
|
||
func refund(api: OrderServing, scenicId: Int?, orderNumber: String, reason: String) async -> Bool {
|
||
let normalizedOrderNumber = orderNumber.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
let normalizedReason = reason.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !normalizedOrderNumber.isEmpty, operatingOrderNumber == nil else { return false }
|
||
guard !normalizedReason.isEmpty else {
|
||
errorMessage = "请填写退款原因"
|
||
return false
|
||
}
|
||
guard let scenicId, scenicId > 0 else {
|
||
errorMessage = "缺少景区上下文"
|
||
return false
|
||
}
|
||
|
||
operatingOrderNumber = normalizedOrderNumber
|
||
defer { operatingOrderNumber = nil }
|
||
|
||
do {
|
||
try await api.depositOrderRefund(orderNumber: normalizedOrderNumber, refundReason: normalizedReason)
|
||
await reload(api: api, scenicId: scenicId, reset: true)
|
||
return true
|
||
} catch {
|
||
errorMessage = error.localizedDescription
|
||
return false
|
||
}
|
||
}
|
||
|
||
/// 清理错误文案。
|
||
func clearError() {
|
||
errorMessage = nil
|
||
}
|
||
|
||
/// 重置State状态。
|
||
private func resetState() {
|
||
orders = []
|
||
total = 0
|
||
page = 1
|
||
hasMore = false
|
||
loading = false
|
||
loadingMore = false
|
||
operatingOrderNumber = nil
|
||
errorMessage = nil
|
||
}
|
||
}
|
||
|
||
@MainActor
|
||
/// 押金订单详情 ViewModel,负责按门店和订单号加载详情。
|
||
final class DepositOrderDetailViewModel {
|
||
var onChange: (() -> Void)?
|
||
private(set) var detail: StoreOrderDetailResponse? { didSet { onChange?() } }
|
||
private(set) var loading = false { didSet { onChange?() } }
|
||
var errorMessage: String? { didSet { onChange?() } }
|
||
|
||
/// 加载押金订单详情,缺少门店或订单号时不请求接口。
|
||
func load(api: OrderServing, storeId: Int?, orderNumber: String) async {
|
||
let normalized = orderNumber.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard let storeId, storeId > 0 else {
|
||
reset(message: "当前账号缺少门店信息")
|
||
return
|
||
}
|
||
guard !normalized.isEmpty else {
|
||
reset(message: "订单号不能为空")
|
||
return
|
||
}
|
||
|
||
detail = nil
|
||
loading = true
|
||
defer { loading = false }
|
||
|
||
do {
|
||
detail = try await api.storeOrderDetail(storeId: storeId, orderNumber: normalized)
|
||
errorMessage = nil
|
||
} catch {
|
||
detail = nil
|
||
errorMessage = error.localizedDescription
|
||
}
|
||
}
|
||
|
||
/// 清理错误文案。
|
||
func clearError() {
|
||
errorMessage = nil
|
||
}
|
||
|
||
/// 重置状态。
|
||
private func reset(message: String) {
|
||
detail = nil
|
||
loading = false
|
||
errorMessage = message
|
||
}
|
||
}
|
||
|
||
@MainActor
|
||
/// 押金拍摄信息 ViewModel,负责加载单个打卡点的媒体和评分。
|
||
final class DepositOrderShootingInfoViewModel {
|
||
var onChange: (() -> Void)?
|
||
private(set) var detail: StoreOrderShootingDetailResponse? { didSet { onChange?() } }
|
||
private(set) var loading = false { didSet { onChange?() } }
|
||
var errorMessage: String? { didSet { onChange?() } }
|
||
|
||
/// 加载押金订单拍摄信息,缺少门店或订单号时不请求接口。
|
||
func load(api: OrderServing, storeId: Int?, orderNumber: String, scenicSpotId: Int, photogUid: Int) async {
|
||
let normalized = orderNumber.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard let storeId, storeId > 0 else {
|
||
reset(message: "当前账号缺少门店信息")
|
||
return
|
||
}
|
||
guard !normalized.isEmpty else {
|
||
reset(message: "订单号不能为空")
|
||
return
|
||
}
|
||
|
||
detail = nil
|
||
loading = true
|
||
defer { loading = false }
|
||
|
||
do {
|
||
detail = try await api.storeOrderShootingDetail(
|
||
storeId: storeId,
|
||
orderNumber: normalized,
|
||
scenicSpotId: scenicSpotId,
|
||
photogUid: photogUid
|
||
)
|
||
errorMessage = nil
|
||
} catch {
|
||
detail = nil
|
||
errorMessage = error.localizedDescription
|
||
}
|
||
}
|
||
|
||
/// 清理错误文案。
|
||
func clearError() {
|
||
errorMessage = nil
|
||
}
|
||
|
||
/// 重置状态。
|
||
private func reset(message: String) {
|
||
detail = nil
|
||
loading = false
|
||
errorMessage = message
|
||
}
|
||
}
|
||
|
||
@MainActor
|
||
/// 普通订单退款 ViewModel,负责退款入口判断、金额校验和提交保护。
|
||
final class OrderRefundViewModel {
|
||
var onChange: (() -> Void)?
|
||
var mode: OrderRefundMode = .full { didSet { onChange?() } }
|
||
var amount = "" { didSet { onChange?() } }
|
||
var reason = "" { didSet { onChange?() } }
|
||
private(set) var submitting = false { didSet { onChange?() } }
|
||
var errorMessage: String? { didSet { onChange?() } }
|
||
|
||
/// 判断订单是否允许展示普通退款入口。
|
||
func canRefund(_ item: OrderEntity) -> Bool {
|
||
(item.orderStatus == 18 || item.orderStatus == 30) &&
|
||
item.orderType != 19 &&
|
||
availableAmount(for: item) > 0
|
||
}
|
||
|
||
/// 打开退款表单时初始化默认值。
|
||
func begin(item: OrderEntity) {
|
||
mode = .full
|
||
amount = availableAmountText(for: item)
|
||
reason = ""
|
||
errorMessage = nil
|
||
}
|
||
|
||
/// 提交普通订单退款。
|
||
func submit(api: OrderServing, item: OrderEntity) async -> Bool {
|
||
guard !submitting else { return false }
|
||
guard canRefund(item) else {
|
||
errorMessage = "当前订单不可退款"
|
||
return false
|
||
}
|
||
let normalizedReason = reason.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !normalizedReason.isEmpty else {
|
||
errorMessage = "请输入退款原因"
|
||
return false
|
||
}
|
||
|
||
let refundAmount: String
|
||
switch mode {
|
||
case .full:
|
||
refundAmount = availableAmountText(for: item)
|
||
case .partial:
|
||
guard let normalized = Self.normalizedMoney(amount) else {
|
||
errorMessage = "请输入有效的退款金额"
|
||
return false
|
||
}
|
||
guard (Decimal(string: normalized) ?? 0) <= availableAmount(for: item) else {
|
||
errorMessage = "退款金额不能大于可退金额"
|
||
return false
|
||
}
|
||
refundAmount = normalized
|
||
}
|
||
|
||
submitting = true
|
||
errorMessage = nil
|
||
defer { submitting = false }
|
||
|
||
do {
|
||
try await api.orderRefund(
|
||
orderNumber: item.orderNumber,
|
||
refundType: mode,
|
||
refundAmount: refundAmount,
|
||
refundReason: normalizedReason
|
||
)
|
||
return true
|
||
} catch {
|
||
errorMessage = error.localizedDescription
|
||
return false
|
||
}
|
||
}
|
||
|
||
/// 计算普通订单可退金额。
|
||
func availableAmount(for item: OrderEntity) -> Decimal {
|
||
let paid = Self.decimalValue(item.actualPayAmount)
|
||
if paid > 0 { return paid }
|
||
return Self.decimalValue(item.actualRefundAmount.isEmpty ? item.refundAmount : item.actualRefundAmount)
|
||
}
|
||
|
||
/// 返回可退金额展示和提交文案。
|
||
func availableAmountText(for item: OrderEntity) -> String {
|
||
Self.moneyText(availableAmount(for: item))
|
||
}
|
||
|
||
/// 将输入金额规范成两位小数,空值、非法值和超过两位小数返回 nil。
|
||
static func normalizedMoney(_ rawValue: String) -> String? {
|
||
let text = rawValue.trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(of: ",", with: "")
|
||
guard !text.isEmpty else { return nil }
|
||
let pattern = #"^\d+(\.\d{1,2})?$"#
|
||
guard text.range(of: pattern, options: .regularExpression) != nil,
|
||
let value = Decimal(string: text),
|
||
value > 0 else {
|
||
return nil
|
||
}
|
||
return moneyText(value)
|
||
}
|
||
|
||
/// decimal值相关逻辑。
|
||
private static func decimalValue(_ text: String) -> Decimal {
|
||
Decimal(string: text.trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(of: ",", with: "")) ?? 0
|
||
}
|
||
|
||
/// money文本相关逻辑。
|
||
private static func moneyText(_ value: Decimal) -> String {
|
||
let number = NSDecimalNumber(decimal: value)
|
||
let handler = NSDecimalNumberHandler(
|
||
roundingMode: .plain,
|
||
scale: 2,
|
||
raiseOnExactness: false,
|
||
raiseOnOverflow: false,
|
||
raiseOnUnderflow: false,
|
||
raiseOnDivideByZero: false
|
||
)
|
||
return String(format: "%.2f", number.rounding(accordingToBehavior: handler).doubleValue)
|
||
}
|
||
}
|
||
|
||
@MainActor
|
||
/// 历史拍摄 ViewModel,负责按订单号加载多点位历史拍摄媒体。
|
||
final class HistoricalShootingInfoViewModel {
|
||
var onChange: (() -> Void)?
|
||
private(set) var projectName = "" { didSet { onChange?() } }
|
||
private(set) var projectTypeName = "" { didSet { onChange?() } }
|
||
private(set) var spots: [PhotogSpotItem] = [] { didSet { onChange?() } }
|
||
private(set) var loading = false { didSet { onChange?() } }
|
||
var errorMessage: String? { didSet { onChange?() } }
|
||
|
||
/// 加载历史拍摄信息,订单号为空时清空旧数据且不请求接口。
|
||
func load(api: OrderServing, orderNumber: String) async {
|
||
let normalized = orderNumber.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !normalized.isEmpty else {
|
||
reset(message: "订单号不能为空")
|
||
return
|
||
}
|
||
|
||
loading = true
|
||
defer { loading = false }
|
||
|
||
do {
|
||
let result = try await api.multiTravelShootHistory(orderNumber: normalized)
|
||
projectName = result.projectName
|
||
projectTypeName = result.projectTypeName
|
||
spots = result.photogSpotList
|
||
errorMessage = nil
|
||
} catch {
|
||
reset(message: error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
/// 清理错误文案。
|
||
func clearError() {
|
||
errorMessage = nil
|
||
}
|
||
|
||
/// 重置状态。
|
||
private func reset(message: String) {
|
||
projectName = ""
|
||
projectTypeName = ""
|
||
spots = []
|
||
loading = false
|
||
errorMessage = message
|
||
}
|
||
}
|
||
|
||
@MainActor
|
||
/// 多点旅拍任务上传 ViewModel,负责订单号、打卡点、云盘附件、本地附件和素材提交。
|
||
final class MultiTravelTaskUploadViewModel {
|
||
var onChange: (() -> Void)?
|
||
var orderNumber = "" { didSet { onChange?() } }
|
||
var selectedSpotId: Int? { didSet { onChange?() } }
|
||
private(set) var spots: [MultiTravelVerifiedScenicSpotItem] = [] { didSet { onChange?() } }
|
||
var selectedCloudFiles: [TaskCloudSelectionItem] = [] { didSet { onChange?() } }
|
||
var selectedLocalFiles: [TaskLocalUploadItem] = [] { didSet { onChange?() } }
|
||
private(set) var isLoadingSpots = false { didSet { onChange?() } }
|
||
private(set) var isSubmitting = false { didSet { onChange?() } }
|
||
private(set) var didSubmitSuccessfully = false { didSet { onChange?() } }
|
||
var errorMessage: String? { didSet { onChange?() } }
|
||
private var loadedSpotOrderNumber = ""
|
||
|
||
/// 当前选中打卡点展示名。
|
||
var selectedSpotName: String {
|
||
spots.first(where: { $0.id == selectedSpotId })?.name ?? "请选择打卡点"
|
||
}
|
||
|
||
/// 判断提交按钮是否满足基础启用条件。
|
||
var canSubmit: Bool {
|
||
!isSubmitting &&
|
||
selectedSpotId != nil &&
|
||
(!selectedCloudFiles.isEmpty || !selectedLocalFiles.isEmpty) &&
|
||
!selectedLocalFiles.contains(where: \.isUploading)
|
||
}
|
||
|
||
/// 初始化任务上传表单订单号。
|
||
init(initialOrderNumber: String = "") {
|
||
orderNumber = initialOrderNumber
|
||
}
|
||
|
||
/// 加载当前订单已核销打卡点,订单号为空时不请求接口。
|
||
func loadSpots(api: OrderServing) async {
|
||
let normalized = orderNumber.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !normalized.isEmpty else {
|
||
resetSpotSelection()
|
||
return
|
||
}
|
||
|
||
isLoadingSpots = true
|
||
defer { isLoadingSpots = false }
|
||
|
||
do {
|
||
let result = try await api.multiTravelVerifiedScenicSpotList(orderNumber: normalized)
|
||
spots = result
|
||
loadedSpotOrderNumber = normalized
|
||
if !spots.contains(where: { $0.id == selectedSpotId }) {
|
||
selectedSpotId = spots.first?.id
|
||
}
|
||
errorMessage = nil
|
||
} catch {
|
||
resetSpotSelection()
|
||
errorMessage = error.localizedDescription
|
||
}
|
||
}
|
||
|
||
/// 切换选中的打卡点。
|
||
func selectSpot(id: Int?) {
|
||
selectedSpotId = id
|
||
}
|
||
|
||
/// 合并云盘附件,按文件 ID 去重。
|
||
func mergeCloudFiles(_ files: [TaskCloudSelectionItem]) {
|
||
var next = selectedCloudFiles
|
||
for file in files where !next.contains(where: { $0.id == file.id }) {
|
||
next.append(file)
|
||
}
|
||
selectedCloudFiles = next
|
||
}
|
||
|
||
/// 移除指定云盘附件。
|
||
func removeCloudFile(id: Int) {
|
||
selectedCloudFiles.removeAll { $0.id == id }
|
||
}
|
||
|
||
/// 添加本地图片或视频附件。
|
||
func addLocalFile(data: Data, fileName: String) {
|
||
selectedLocalFiles.append(
|
||
TaskLocalUploadItem(
|
||
id: UUID(),
|
||
data: data,
|
||
fileName: fileName,
|
||
fileType: Self.fileType(for: fileName),
|
||
remark: "",
|
||
uploadedURL: nil,
|
||
progress: 0,
|
||
errorMessage: nil
|
||
)
|
||
)
|
||
}
|
||
|
||
/// 移除指定本地附件。
|
||
func removeLocalFile(id: UUID) {
|
||
selectedLocalFiles.removeAll { $0.id == id }
|
||
}
|
||
|
||
/// 提交多点旅拍任务素材;本地附件先上传 OSS,再提交最终 URL。
|
||
@discardableResult
|
||
func submit(api: OrderServing, uploadService: any OSSUploadServing, scenicId: Int?) async -> Bool {
|
||
guard !isSubmitting else { return false }
|
||
guard let requestContext = validateBeforeSubmit(scenicId: scenicId) else { return false }
|
||
|
||
isSubmitting = true
|
||
didSubmitSuccessfully = false
|
||
errorMessage = nil
|
||
defer { isSubmitting = false }
|
||
|
||
do {
|
||
let uploadFiles = try await uploadLocalFiles(uploadService: uploadService, scenicId: requestContext.scenicId)
|
||
try await api.multiTravelUploadMaterial(
|
||
MultiTravelUploadMaterialRequest(
|
||
orderNumber: requestContext.orderNumber,
|
||
scenicSpotId: requestContext.scenicSpotId,
|
||
cloudFile: selectedCloudFiles.map { MultiTravelCloudFileItem(fileId: $0.id) },
|
||
uploadFile: uploadFiles
|
||
)
|
||
)
|
||
didSubmitSuccessfully = true
|
||
return true
|
||
} catch {
|
||
errorMessage = error.localizedDescription
|
||
return false
|
||
}
|
||
}
|
||
|
||
/// 清理错误文案。
|
||
func clearError() {
|
||
errorMessage = nil
|
||
}
|
||
|
||
/// 校验BeforeSubmit输入或状态。
|
||
private func validateBeforeSubmit(scenicId: Int?) -> SubmitContext? {
|
||
let normalizedOrderNumber = orderNumber.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard let scenicId, scenicId > 0 else {
|
||
errorMessage = "请先选择景区"
|
||
return nil
|
||
}
|
||
guard !normalizedOrderNumber.isEmpty else {
|
||
errorMessage = "请先填写订单号"
|
||
return nil
|
||
}
|
||
guard loadedSpotOrderNumber == normalizedOrderNumber else {
|
||
errorMessage = "请先刷新打卡点"
|
||
return nil
|
||
}
|
||
guard let selectedSpotId else {
|
||
errorMessage = "请选择打卡点"
|
||
return nil
|
||
}
|
||
guard !selectedCloudFiles.isEmpty || !selectedLocalFiles.isEmpty else {
|
||
errorMessage = "请至少选择一个素材文件"
|
||
return nil
|
||
}
|
||
guard !selectedLocalFiles.contains(where: \.isUploading) else {
|
||
errorMessage = "文件上传中,请稍后提交"
|
||
return nil
|
||
}
|
||
return SubmitContext(scenicId: scenicId, orderNumber: normalizedOrderNumber, scenicSpotId: selectedSpotId)
|
||
}
|
||
|
||
/// upload本地Files相关逻辑。
|
||
private func uploadLocalFiles(uploadService: any OSSUploadServing, scenicId: Int) async throws -> [MultiTravelUploadFileItem] {
|
||
var uploadFiles: [MultiTravelUploadFileItem] = []
|
||
for index in selectedLocalFiles.indices {
|
||
if let uploadedURL = selectedLocalFiles[index].uploadedURL {
|
||
uploadFiles.append(MultiTravelUploadFileItem(fileName: selectedLocalFiles[index].fileName, fileUrl: uploadedURL))
|
||
continue
|
||
}
|
||
|
||
selectedLocalFiles[index].errorMessage = nil
|
||
let fileID = selectedLocalFiles[index].id
|
||
do {
|
||
let url = try await uploadService.uploadTaskFile(
|
||
data: selectedLocalFiles[index].data,
|
||
fileName: selectedLocalFiles[index].fileName,
|
||
fileType: selectedLocalFiles[index].fileType,
|
||
scenicId: scenicId,
|
||
onProgress: { [weak self] progress in
|
||
Task { @MainActor in
|
||
self?.updateLocalFileProgress(id: fileID, progress: progress)
|
||
}
|
||
}
|
||
)
|
||
selectedLocalFiles[index].uploadedURL = url
|
||
selectedLocalFiles[index].progress = 100
|
||
uploadFiles.append(MultiTravelUploadFileItem(fileName: selectedLocalFiles[index].fileName, fileUrl: url))
|
||
} catch {
|
||
selectedLocalFiles[index].progress = 0
|
||
selectedLocalFiles[index].errorMessage = error.localizedDescription
|
||
throw error
|
||
}
|
||
}
|
||
return uploadFiles
|
||
}
|
||
|
||
/// 更新LocalFileProgress状态。
|
||
private func updateLocalFileProgress(id: UUID, progress: Int) {
|
||
guard let index = selectedLocalFiles.firstIndex(where: { $0.id == id }) else { return }
|
||
selectedLocalFiles[index].progress = progress
|
||
}
|
||
|
||
/// 重置SpotSelection状态。
|
||
private func resetSpotSelection() {
|
||
spots = []
|
||
selectedSpotId = nil
|
||
loadedSpotOrderNumber = ""
|
||
isLoadingSpots = false
|
||
}
|
||
|
||
/// fileType相关逻辑。
|
||
private static func fileType(for fileName: String) -> Int {
|
||
let ext = URL(fileURLWithPath: fileName).pathExtension.lowercased()
|
||
return ["mp4", "mov", "m4v", "avi"].contains(ext) ? 1 : 2
|
||
}
|
||
|
||
/// Submit上下文,持有跨页面共享状态。
|
||
private struct SubmitContext {
|
||
let scenicId: Int
|
||
let orderNumber: String
|
||
let scenicSpotId: Int
|
||
}
|
||
}
|