Initial commit: suixinkan_ios UIKit rewrite project.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,80 @@
|
||||
//
|
||||
// LocationReportAPI.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 位置上报服务协议,抽象上报和历史接口以便 ViewModel 测试替换。
|
||||
@MainActor
|
||||
protocol LocationReportServing {
|
||||
/// 提交一次位置上报。
|
||||
func reportLocation(staffId: Int, latitude: Double, longitude: Double, address: String, type: LocationReportType, scenicId: Int) async throws -> LocationReportSubmitResponse
|
||||
|
||||
/// 获取位置上报历史列表。
|
||||
func locationReportList(staffId: Int, page: Int, pageSize: Int, type: LocationReportType, startDate: String?, endDate: String?) async throws -> ListPayload<LocationReportHistoryItem>
|
||||
}
|
||||
|
||||
/// 位置上报 API,负责封装旧工程定位上报相关接口。
|
||||
@MainActor
|
||||
final class LocationReportAPI: LocationReportServing {
|
||||
private let client: APIClient
|
||||
|
||||
/// 初始化位置上报 API,并注入共享网络客户端。
|
||||
init(client: APIClient) {
|
||||
self.client = client
|
||||
}
|
||||
|
||||
/// 提交一次位置上报。
|
||||
func reportLocation(
|
||||
staffId: Int,
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
address: String,
|
||||
type: LocationReportType,
|
||||
scenicId: Int
|
||||
) async throws -> LocationReportSubmitResponse {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .post,
|
||||
path: "/api/yf-handset-app/photog/loacation/report",
|
||||
queryItems: [
|
||||
URLQueryItem(name: "staff_id", value: "\(staffId)"),
|
||||
URLQueryItem(name: "latitude", value: "\(latitude)"),
|
||||
URLQueryItem(name: "longitude", value: "\(longitude)"),
|
||||
URLQueryItem(name: "address", value: address),
|
||||
URLQueryItem(name: "type", value: "\(type.rawValue)"),
|
||||
URLQueryItem(name: "scenic_id", value: "\(scenicId)")
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 获取位置上报历史列表。
|
||||
func locationReportList(
|
||||
staffId: Int,
|
||||
page: Int = 1,
|
||||
pageSize: Int = 20,
|
||||
type: LocationReportType = .all,
|
||||
startDate: String? = nil,
|
||||
endDate: String? = nil
|
||||
) async throws -> ListPayload<LocationReportHistoryItem> {
|
||||
var query = [
|
||||
URLQueryItem(name: "staff_id", value: "\(staffId)"),
|
||||
URLQueryItem(name: "page", value: "\(max(page, 1))"),
|
||||
URLQueryItem(name: "page_size", value: "\(max(pageSize, 1))"),
|
||||
URLQueryItem(name: "type", value: "\(max(type.rawValue, 0))")
|
||||
]
|
||||
if let startDate, !startDate.isEmpty {
|
||||
query.append(URLQueryItem(name: "start_date", value: startDate))
|
||||
}
|
||||
if let endDate, !endDate.isEmpty {
|
||||
query.append(URLQueryItem(name: "end_date", value: endDate))
|
||||
}
|
||||
return try await client.send(
|
||||
APIRequest(method: .get, path: "/api/yf-handset-app/photog/loacation/list", queryItems: query)
|
||||
)
|
||||
}
|
||||
}
|
||||
39
suixinkan_ios/Features/LocationReport/LocationReport.md
Normal file
39
suixinkan_ios/Features/LocationReport/LocationReport.md
Normal file
@ -0,0 +1,39 @@
|
||||
# LocationReport 模块业务逻辑
|
||||
|
||||
## 模块职责
|
||||
|
||||
LocationReport 模块负责首页 `location_report` 和 `location_report_history` 入口。
|
||||
|
||||
本模块包含当前位置获取、标记点、在线状态、立即上报、提醒设置、历史记录、类型筛选、日期筛选和分页。上报状态只保存在页面 ViewModel 内,不进入全局 App 状态。
|
||||
|
||||
## 数据来源
|
||||
|
||||
- 当前景区 ID 从 `AccountContext.currentScenic` 读取。
|
||||
- 上报人员 ID 从 `AccountSnapshotStore.load()?.businessUserId` 读取,避免把业务账号 ID 复制进页面全局状态。
|
||||
- 提交接口使用旧工程拼写 `/api/yf-handset-app/photog/loacation/report`。
|
||||
- 历史接口使用 `/api/yf-handset-app/photog/loacation/list`。
|
||||
|
||||
## 上报流程
|
||||
|
||||
`LocationReportViewModel` 管理当前位置、标记点、在线状态、提醒分钟数和页面内倒计时。
|
||||
|
||||
1. 页面进入时请求一次前台定位。
|
||||
2. 用户可以立即上报当前位置,也可以把当前位置设为标记点后上报。
|
||||
3. 切换到在线状态时提交一次在线状态上报。
|
||||
4. 上报成功后按服务端 `expired` 重置倒计时;服务端未返回时兜底为 2 小时。
|
||||
|
||||
## 历史流程
|
||||
|
||||
`LocationReportHistoryViewModel` 管理类型筛选、日期筛选、分页和错误状态。
|
||||
|
||||
历史记录支持 `全部`、`立即上报`、`标记点`、`在线状态` 四类筛选。分页失败只保留错误提示,不影响登录态和其他模块。
|
||||
|
||||
## 定位边界
|
||||
|
||||
当前只做前台手动定位和页面内倒计时。本轮不做后台持续定位、后台定时上报、离线持久化队列或推送提醒。
|
||||
|
||||
定位结果、在线状态、提醒倒计时和提交表单均不落盘。
|
||||
|
||||
## 测试要求
|
||||
|
||||
新增位置上报逻辑时,需要同步补充 API、ViewModel 和路由测试。测试失败时先修复问题,再继续迁移后续功能。
|
||||
@ -0,0 +1,127 @@
|
||||
//
|
||||
// LocationReportModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 位置上报类型实体,表示立即上报、标记点上报和在线状态上报。
|
||||
enum LocationReportType: Int, CaseIterable, Identifiable {
|
||||
case all = 0
|
||||
case immediate = 1
|
||||
case marked = 2
|
||||
case onlineStatus = 3
|
||||
|
||||
var id: Int { rawValue }
|
||||
|
||||
/// 上报类型展示标题。
|
||||
var title: String {
|
||||
switch self {
|
||||
case .all: "全部"
|
||||
case .immediate: "立即上报"
|
||||
case .marked: "标记点"
|
||||
case .onlineStatus: "在线状态"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 位置坐标实体,表示定位或手动选点得到的经纬度。
|
||||
struct LocationCoordinate: Equatable {
|
||||
var latitude: Double
|
||||
var longitude: Double
|
||||
}
|
||||
|
||||
/// 位置上报提交响应实体,表示服务端返回的上报人员、过期秒数和状态。
|
||||
struct LocationReportSubmitResponse: Decodable, Equatable {
|
||||
let staffId: String
|
||||
let expired: Int
|
||||
let status: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case staffId = "staff_id"
|
||||
case expired
|
||||
case status
|
||||
}
|
||||
|
||||
/// 创建位置上报提交响应,主要用于测试替身返回。
|
||||
init(staffId: String, expired: Int, status: Int) {
|
||||
self.staffId = staffId
|
||||
self.expired = expired
|
||||
self.status = status
|
||||
}
|
||||
|
||||
/// 宽松解码提交响应字段。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
staffId = try container.decodeLossyString(forKey: .staffId)
|
||||
expired = try container.decodeLossyInt(forKey: .expired) ?? 0
|
||||
status = try container.decodeLossyInt(forKey: .status) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
/// 位置上报历史项实体,表示一次历史上报记录。
|
||||
struct LocationReportHistoryItem: Decodable, Equatable, Identifiable {
|
||||
let id: Int
|
||||
let staffId: Int
|
||||
let type: Int
|
||||
let latitude: String
|
||||
let longitude: String
|
||||
let address: String
|
||||
let ip: String
|
||||
let remark: String
|
||||
let createdAt: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case staffId = "staff_id"
|
||||
case type
|
||||
case latitude
|
||||
case longitude
|
||||
case address
|
||||
case ip
|
||||
case remark
|
||||
case createdAt = "created_at"
|
||||
}
|
||||
|
||||
/// 宽松解码历史项字段。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeLossyInt(forKey: .id) ?? 0
|
||||
staffId = try container.decodeLossyInt(forKey: .staffId) ?? 0
|
||||
type = try container.decodeLossyInt(forKey: .type) ?? 0
|
||||
latitude = try container.decodeLossyString(forKey: .latitude)
|
||||
longitude = try container.decodeLossyString(forKey: .longitude)
|
||||
address = try container.decodeLossyString(forKey: .address)
|
||||
ip = try container.decodeLossyString(forKey: .ip)
|
||||
remark = try container.decodeLossyString(forKey: .remark)
|
||||
createdAt = try container.decodeLossyString(forKey: .createdAt)
|
||||
}
|
||||
|
||||
/// 上报类型展示文案。
|
||||
var typeTitle: String {
|
||||
LocationReportType(rawValue: type)?.title ?? "未知"
|
||||
}
|
||||
}
|
||||
|
||||
private extension KeyedDecodingContainer {
|
||||
/// 将 String、数字和 Bool 宽松解码为字符串。
|
||||
func decodeLossyString(forKey key: Key) throws -> String {
|
||||
if let value = try? decodeIfPresent(String.self, forKey: key) { return value }
|
||||
if let value = try? decodeIfPresent(Int.self, forKey: key) { return String(value) }
|
||||
if let value = try? decodeIfPresent(Double.self, forKey: key) { return String(value) }
|
||||
if let value = try? decodeIfPresent(Bool.self, forKey: key) { return value ? "1" : "0" }
|
||||
return ""
|
||||
}
|
||||
|
||||
/// 将 String、Double 和 Int 宽松解码为 Int。
|
||||
func decodeLossyInt(forKey key: Key) throws -> Int? {
|
||||
if let value = try? decodeIfPresent(Int.self, forKey: key) { return value }
|
||||
if let value = try? decodeIfPresent(Double.self, forKey: key) { return Int(value) }
|
||||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||||
return Int(value.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
//
|
||||
// LocationReportViewControllers.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// 位置上报页。
|
||||
final class LocationReportViewController: ModuleTableViewController {
|
||||
private let viewModel = LocationReportViewModel()
|
||||
|
||||
override func viewDidLoad() {
|
||||
title = "位置上报"
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
title: "上报",
|
||||
style: .done,
|
||||
target: self,
|
||||
action: #selector(submitReport)
|
||||
)
|
||||
super.viewDidLoad()
|
||||
wireViewModel(viewModel) { }
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int { 2 }
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
section == 0 ? 3 : 1
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
section == 0 ? "状态" : "操作"
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(
|
||||
withIdentifier: TitleSubtitleTableViewCell.reuseIdentifier,
|
||||
for: indexPath
|
||||
) as! TitleSubtitleTableViewCell
|
||||
if indexPath.section == 0 {
|
||||
switch indexPath.row {
|
||||
case 0:
|
||||
cell.configure(title: "在线状态", subtitle: viewModel.isOnline ? "在线" : "离线")
|
||||
case 1:
|
||||
cell.configure(title: "当前地址", subtitle: viewModel.currentAddress)
|
||||
case 2:
|
||||
cell.configure(title: "倒计时", subtitle: viewModel.countdownText)
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else {
|
||||
cell.configure(title: "切换在线状态", subtitle: viewModel.isOnline ? "点击离线" : "点击上线")
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
if indexPath.section == 1 {
|
||||
Task {
|
||||
_ = await viewModel.setOnline(
|
||||
!viewModel.isOnline,
|
||||
staffId: services.staffId,
|
||||
scenicId: services.currentScenicId,
|
||||
api: services.locationReportAPI
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func reloadContent() async {
|
||||
viewModel.applyCurrentLocation(latitude: 39.9, longitude: 116.4, address: "定位待接入")
|
||||
}
|
||||
|
||||
@objc private func submitReport() {
|
||||
Task {
|
||||
_ = await viewModel.submit(
|
||||
type: .immediate,
|
||||
staffId: services.staffId,
|
||||
scenicId: services.currentScenicId,
|
||||
api: services.locationReportAPI
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LocationReportViewModel: ViewModelBindable {}
|
||||
|
||||
/// 位置上报历史页。
|
||||
final class LocationReportHistoryViewController: ModuleTableViewController {
|
||||
private let viewModel = LocationReportHistoryViewModel()
|
||||
|
||||
override func viewDidLoad() {
|
||||
title = "上报历史"
|
||||
super.viewDidLoad()
|
||||
wireViewModel(viewModel) { }
|
||||
}
|
||||
|
||||
override func tableRowCount() -> Int {
|
||||
viewModel.items.count
|
||||
}
|
||||
|
||||
override func configureCell(_ cell: TitleSubtitleTableViewCell, at indexPath: IndexPath) {
|
||||
let record = viewModel.items[indexPath.row]
|
||||
cell.configure(title: record.typeTitle, subtitle: record.address, detail: record.createdAt)
|
||||
}
|
||||
|
||||
override func reloadContent() async {
|
||||
await viewModel.reload(staffId: services.staffId, api: services.locationReportAPI)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
guard indexPath.row >= viewModel.items.count - 2 else { return }
|
||||
Task { await viewModel.loadMore(staffId: services.staffId, api: services.locationReportAPI) }
|
||||
}
|
||||
}
|
||||
|
||||
extension LocationReportHistoryViewModel: ViewModelBindable {}
|
||||
@ -0,0 +1,232 @@
|
||||
//
|
||||
// LocationReportViewModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 位置上报 ViewModel,负责页面内在线状态、坐标、倒计时和提交动作。
|
||||
@MainActor
|
||||
final class LocationReportViewModel {
|
||||
var onChange: (() -> Void)?
|
||||
var isOnline = false { didSet { onChange?() } }
|
||||
var reminderMinutes = 30 { didSet { onChange?() } }
|
||||
var secondsUntilReport = 0 { didSet { onChange?() } }
|
||||
var currentCoordinate: LocationCoordinate? { didSet { onChange?() } }
|
||||
var markedCoordinate: LocationCoordinate? { didSet { onChange?() } }
|
||||
var currentAddress = "" { didSet { onChange?() } }
|
||||
var markedAddress = "" { didSet { onChange?() } }
|
||||
var lastReportText = "" { didSet { onChange?() } }
|
||||
var errorMessage: String? { didSet { onChange?() } }
|
||||
var isSubmitting = false { didSet { onChange?() } }
|
||||
|
||||
/// 倒计时展示文案。
|
||||
var countdownText: String {
|
||||
guard secondsUntilReport > 0 else { return "可立即上报" }
|
||||
let hours = secondsUntilReport / 3600
|
||||
let minutes = (secondsUntilReport % 3600) / 60
|
||||
return "\(hours)小时\(minutes)分钟后可再次提醒"
|
||||
}
|
||||
|
||||
/// 设置当前位置。
|
||||
func applyCurrentLocation(latitude: Double, longitude: Double, address: String) {
|
||||
currentCoordinate = LocationCoordinate(latitude: latitude, longitude: longitude)
|
||||
currentAddress = address
|
||||
}
|
||||
|
||||
/// 设置标记点位置。
|
||||
func applyMarkedLocation(latitude: Double, longitude: Double, address: String) {
|
||||
markedCoordinate = LocationCoordinate(latitude: latitude, longitude: longitude)
|
||||
markedAddress = address
|
||||
}
|
||||
|
||||
/// 切换在线状态;切到在线时会提交一次状态上报。
|
||||
func setOnline(
|
||||
_ value: Bool,
|
||||
staffId: Int?,
|
||||
scenicId: Int?,
|
||||
api: any LocationReportServing
|
||||
) async -> Bool {
|
||||
isOnline = value
|
||||
guard value else { return true }
|
||||
return await submit(type: .onlineStatus, staffId: staffId, scenicId: scenicId, api: api)
|
||||
}
|
||||
|
||||
/// 提交指定类型的位置上报。
|
||||
func submit(
|
||||
type: LocationReportType,
|
||||
staffId: Int?,
|
||||
scenicId: Int?,
|
||||
api: any LocationReportServing
|
||||
) async -> Bool {
|
||||
guard !isSubmitting else { return false }
|
||||
guard let staffId, staffId > 0 else {
|
||||
errorMessage = "缺少上报人员"
|
||||
return false
|
||||
}
|
||||
guard let scenicId, scenicId > 0 else {
|
||||
errorMessage = "缺少当前景区"
|
||||
return false
|
||||
}
|
||||
|
||||
let source = reportSource(for: type)
|
||||
guard let coordinate = source.coordinate else {
|
||||
errorMessage = "请先获取或选择位置"
|
||||
return false
|
||||
}
|
||||
let address = source.address.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !address.isEmpty else {
|
||||
errorMessage = "请填写位置地址"
|
||||
return false
|
||||
}
|
||||
|
||||
isSubmitting = true
|
||||
errorMessage = nil
|
||||
defer { isSubmitting = false }
|
||||
|
||||
do {
|
||||
let response = try await api.reportLocation(
|
||||
staffId: staffId,
|
||||
latitude: coordinate.latitude,
|
||||
longitude: coordinate.longitude,
|
||||
address: address,
|
||||
type: type,
|
||||
scenicId: scenicId
|
||||
)
|
||||
secondsUntilReport = response.expired > 0 ? response.expired : 7200
|
||||
lastReportText = LocationReportViewModel.displayFormatter.string(from: Date())
|
||||
return true
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// 页面内倒计时每秒递减。
|
||||
func tick() {
|
||||
guard secondsUntilReport > 0 else { return }
|
||||
secondsUntilReport -= 1
|
||||
}
|
||||
|
||||
/// 根据上报类型选择当前坐标或标记点坐标。
|
||||
private func reportSource(for type: LocationReportType) -> (coordinate: LocationCoordinate?, address: String) {
|
||||
switch type {
|
||||
case .marked:
|
||||
(markedCoordinate ?? currentCoordinate, markedAddress.isEmpty ? currentAddress : markedAddress)
|
||||
case .all, .immediate, .onlineStatus:
|
||||
(currentCoordinate, currentAddress)
|
||||
}
|
||||
}
|
||||
|
||||
private static let displayFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
return formatter
|
||||
}()
|
||||
}
|
||||
|
||||
/// 位置上报历史 ViewModel,负责筛选、日期和分页加载。
|
||||
@MainActor
|
||||
final class LocationReportHistoryViewModel {
|
||||
var onChange: (() -> Void)?
|
||||
var selectedType: LocationReportType = .all { didSet { onChange?() } }
|
||||
var startDate: Date? { didSet { onChange?() } }
|
||||
var endDate: Date? { didSet { onChange?() } }
|
||||
var items: [LocationReportHistoryItem] = [] { didSet { onChange?() } }
|
||||
var errorMessage: String? { didSet { onChange?() } }
|
||||
var isLoading = false { didSet { onChange?() } }
|
||||
var isLoadingMore = false { didSet { onChange?() } }
|
||||
var total = 0 { didSet { onChange?() } }
|
||||
|
||||
private var page = 1
|
||||
private let pageSize = 20
|
||||
|
||||
/// 是否还有下一页历史记录。
|
||||
var hasMore: Bool {
|
||||
items.count < total
|
||||
}
|
||||
|
||||
/// 重新加载历史记录。
|
||||
func reload(staffId: Int?, api: any LocationReportServing) async {
|
||||
guard let staffId, staffId > 0 else {
|
||||
reset()
|
||||
errorMessage = "缺少上报人员"
|
||||
return
|
||||
}
|
||||
page = 1
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
defer { isLoading = false }
|
||||
|
||||
do {
|
||||
let payload = try await api.locationReportList(
|
||||
staffId: staffId,
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
type: selectedType,
|
||||
startDate: dateString(startDate),
|
||||
endDate: dateString(endDate)
|
||||
)
|
||||
items = payload.list
|
||||
total = payload.total
|
||||
} catch {
|
||||
items = []
|
||||
total = 0
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载下一页历史记录。
|
||||
func loadMore(staffId: Int?, api: any LocationReportServing) async {
|
||||
guard let staffId, staffId > 0, hasMore, !isLoading, !isLoadingMore else { return }
|
||||
isLoadingMore = true
|
||||
let nextPage = page + 1
|
||||
defer { isLoadingMore = false }
|
||||
|
||||
do {
|
||||
let payload = try await api.locationReportList(
|
||||
staffId: staffId,
|
||||
page: nextPage,
|
||||
pageSize: pageSize,
|
||||
type: selectedType,
|
||||
startDate: dateString(startDate),
|
||||
endDate: dateString(endDate)
|
||||
)
|
||||
page = nextPage
|
||||
items.append(contentsOf: payload.list)
|
||||
total = payload.total
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换历史类型筛选并刷新。
|
||||
func selectType(_ type: LocationReportType, staffId: Int?, api: any LocationReportServing) async {
|
||||
guard selectedType != type else { return }
|
||||
selectedType = type
|
||||
await reload(staffId: staffId, api: api)
|
||||
}
|
||||
|
||||
/// 清空历史状态。
|
||||
func reset() {
|
||||
items = []
|
||||
total = 0
|
||||
page = 1
|
||||
isLoading = false
|
||||
isLoadingMore = false
|
||||
}
|
||||
|
||||
/// 将日期转为接口需要的 yyyy-MM-dd 字符串。
|
||||
private func dateString(_ date: Date?) -> String? {
|
||||
guard let date else { return nil }
|
||||
return Self.dateFormatter.string(from: date)
|
||||
}
|
||||
|
||||
private static let dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
return formatter
|
||||
}()
|
||||
}
|
||||
Reference in New Issue
Block a user