修复任务提交与弹窗交互问题
This commit is contained in:
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_add_folder.png",
|
"filename" : "icon_cloud_storage_add_folder.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_add_photo.png",
|
"filename" : "icon_cloud_storage_add_photo.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_add_video.png",
|
"filename" : "icon_cloud_storage_add_video.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_current_path_arrow.png",
|
"filename" : "icon_cloud_storage_current_path_arrow.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_file_delete.png",
|
"filename" : "icon_cloud_storage_file_delete.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_file_details.png",
|
"filename" : "icon_cloud_storage_file_details.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_file_download.png",
|
"filename" : "icon_cloud_storage_file_download.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_file_filter.png",
|
"filename" : "icon_cloud_storage_file_filter.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_file_item_details.png",
|
"filename" : "icon_cloud_storage_file_item_details.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_file_move.png",
|
"filename" : "icon_cloud_storage_file_move.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_file_play.png",
|
"filename" : "icon_cloud_storage_file_play.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_file_search.png",
|
"filename" : "icon_cloud_storage_file_search.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_file_share.png",
|
"filename" : "icon_cloud_storage_file_share.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_file_type_folder.png",
|
"filename" : "icon_cloud_storage_file_type_folder.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_file_type_photo.png",
|
"filename" : "icon_cloud_storage_file_type_photo.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_file_type_video.png",
|
"filename" : "icon_cloud_storage_file_type_video.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_folder_type.png",
|
"filename" : "icon_cloud_storage_folder_type.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_go_home.png",
|
"filename" : "icon_cloud_storage_go_home.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_list_sort.png",
|
"filename" : "icon_cloud_storage_list_sort.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_show_type_list.png",
|
"filename" : "icon_cloud_storage_show_type_list.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_show_type_table.png",
|
"filename" : "icon_cloud_storage_show_type_table.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -4,6 +4,14 @@
|
|||||||
"filename" : "icon_cloud_storage_transit.png",
|
"filename" : "icon_cloud_storage_transit.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
12
suixinkan/Assets.xcassets/WildPhotographerReport/report_share_cover.imageset/Contents.json
vendored
Normal file
12
suixinkan/Assets.xcassets/WildPhotographerReport/report_share_cover.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "report_share_cover.jpg",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
suixinkan/Assets.xcassets/WildPhotographerReport/report_share_cover.imageset/report_share_cover.jpg
vendored
Normal file
BIN
suixinkan/Assets.xcassets/WildPhotographerReport/report_share_cover.imageset/report_share_cover.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@ -61,6 +61,14 @@ enum MaterialMediaType: Int, CaseIterable, Sendable, Equatable {
|
|||||||
case .video: 1
|
case .video: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 云盘文件类型,视频为 1、图片为 2。
|
||||||
|
var cloudFileType: Int {
|
||||||
|
switch self {
|
||||||
|
case .image: 2
|
||||||
|
case .video: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 素材列表项,对齐 Android `MaterialItemEntity`。
|
/// 素材列表项,对齐 Android `MaterialItemEntity`。
|
||||||
|
|||||||
@ -354,6 +354,48 @@ final class MaterialFormViewModel {
|
|||||||
notifyStateChange()
|
notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 增加云盘素材文件,文件已在云盘中时直接使用其 URL。
|
||||||
|
func addCloudMediaFiles(_ files: [CloudFile]) {
|
||||||
|
guard !files.isEmpty else { return }
|
||||||
|
let remaining = mediaType.maxCount - uploadedMediaList.count
|
||||||
|
guard remaining > 0 else {
|
||||||
|
onShowMessage?(mediaType == .video ? "视频只能上传1个" : "图片最多上传9张")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var seenCloudIDs = importedCloudFileIDs
|
||||||
|
var accepted: [MaterialUploadedMediaItem] = []
|
||||||
|
for file in files where accepted.count < remaining {
|
||||||
|
let fileURL = file.fileUrl.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard file.type == mediaType.cloudFileType, !fileURL.isEmpty, !seenCloudIDs.contains(file.id) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenCloudIDs.insert(file.id)
|
||||||
|
accepted.append(
|
||||||
|
MaterialUploadedMediaItem(
|
||||||
|
id: "cloud_\(file.id)",
|
||||||
|
data: Data(),
|
||||||
|
thumbnailData: nil,
|
||||||
|
previewURL: file.coverUrl.isEmpty ? fileURL : file.coverUrl,
|
||||||
|
fileName: file.name.isEmpty ? "cloud_file_\(file.id)" : file.name,
|
||||||
|
size: file.fileSize,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
ossUrl: fileURL,
|
||||||
|
isUploading: false,
|
||||||
|
uploadProgress: 100
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !accepted.isEmpty else {
|
||||||
|
onShowMessage?(mediaType == .video ? "请选择视频素材" : "请选择图片素材")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uploadedMediaList.append(contentsOf: accepted)
|
||||||
|
notifyStateChange()
|
||||||
|
}
|
||||||
|
|
||||||
/// 设置封面并立即上传。
|
/// 设置封面并立即上传。
|
||||||
func setCoverImage(_ item: MaterialUploadedMediaItem, uploader: any MaterialOSSUploading) async {
|
func setCoverImage(_ item: MaterialUploadedMediaItem, uploader: any MaterialOSSUploading) async {
|
||||||
coverImage = item
|
coverImage = item
|
||||||
@ -376,6 +418,16 @@ final class MaterialFormViewModel {
|
|||||||
notifyStateChange()
|
notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 已从云盘导入的文件 ID。
|
||||||
|
var importedCloudFileIDs: Set<Int> {
|
||||||
|
Set(
|
||||||
|
uploadedMediaList.compactMap { item in
|
||||||
|
guard item.id.hasPrefix("cloud_") else { return nil }
|
||||||
|
return Int(item.id.dropFirst("cloud_".count))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// 上传或编辑素材。
|
/// 上传或编辑素材。
|
||||||
func submit(api: any MaterialManagementServing) async {
|
func submit(api: any MaterialManagementServing) async {
|
||||||
guard !materialName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
guard !materialName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||||
|
|||||||
@ -61,20 +61,6 @@ final class TaskAPI {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 登记 OSS 上传后的文件 URL。
|
|
||||||
func registerUploadedFileURL(scenicId: Int, fileURL: String, folderId: Int = 0) async throws {
|
|
||||||
let _: EmptyResponse = try await client.send(
|
|
||||||
APIRequest(
|
|
||||||
method: .post,
|
|
||||||
path: "/api/yf-handset-app/photog/album/file-upload-url",
|
|
||||||
queryItems: [
|
|
||||||
URLQueryItem(name: "scenic_id", value: String(scenicId)),
|
|
||||||
URLQueryItem(name: "file_url", value: fileURL),
|
|
||||||
URLQueryItem(name: "folder_id", value: String(folderId)),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 空响应占位,用于无 data 字段的成功接口。
|
/// 空响应占位,用于无 data 字段的成功接口。
|
||||||
|
|||||||
@ -177,4 +177,14 @@ struct TaskFileItem: Sendable, Equatable, Hashable, Identifiable {
|
|||||||
return uploadFileName ?? ""
|
return uploadFileName ?? ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 素材来源角标文案。
|
||||||
|
var sourceBadgeText: String {
|
||||||
|
switch source {
|
||||||
|
case .cloud:
|
||||||
|
return "云盘"
|
||||||
|
case .upload:
|
||||||
|
return "本地"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,9 +25,13 @@ final class CloudStoragePickForTaskViewModel {
|
|||||||
private var totalCount = 0
|
private var totalCount = 0
|
||||||
private let pageSize = 20
|
private let pageSize = 20
|
||||||
private let importedFileIDs: Set<Int>
|
private let importedFileIDs: Set<Int>
|
||||||
|
private let allowedFileType: Int?
|
||||||
|
|
||||||
init(importedFileIDs: Set<Int> = []) {
|
/// 初始化云盘多选 ViewModel,可按云盘文件类型限制选择范围。
|
||||||
|
init(importedFileIDs: Set<Int> = [], allowedFileType: Int? = nil) {
|
||||||
self.importedFileIDs = importedFileIDs
|
self.importedFileIDs = importedFileIDs
|
||||||
|
self.allowedFileType = allowedFileType
|
||||||
|
self.filterType = allowedFileType ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 当前文件夹 ID。
|
/// 当前文件夹 ID。
|
||||||
@ -94,6 +98,10 @@ final class CloudStoragePickForTaskViewModel {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard file.isSelectableMedia else { return }
|
guard file.isSelectableMedia else { return }
|
||||||
|
if let allowedFileType, file.type != allowedFileType {
|
||||||
|
onShowMessage?(allowedFileType == 1 ? "请选择视频素材" : "请选择图片素材")
|
||||||
|
return
|
||||||
|
}
|
||||||
if isImported(file.id) {
|
if isImported(file.id) {
|
||||||
onShowMessage?("已选择")
|
onShowMessage?("已选择")
|
||||||
return
|
return
|
||||||
|
|||||||
@ -139,16 +139,6 @@ final class WildPhotographerReportSubmitViewModel {
|
|||||||
return "当前定位:\(reportLocation)"
|
return "当前定位:\(reportLocation)"
|
||||||
}
|
}
|
||||||
|
|
||||||
var locationSubtitleText: String {
|
|
||||||
if isUpdatingLocation {
|
|
||||||
return "正在更新定位,请稍候"
|
|
||||||
}
|
|
||||||
if locationConfirmed {
|
|
||||||
return "已获取最新定位,如不准确请点击更新定位"
|
|
||||||
}
|
|
||||||
return "如位置不准确,请点击更新定位"
|
|
||||||
}
|
|
||||||
|
|
||||||
init(
|
init(
|
||||||
homeViewModel: WildPhotographerReportHomeViewModel,
|
homeViewModel: WildPhotographerReportHomeViewModel,
|
||||||
locationProvider: any LocationProviding = LocationProvider.shared
|
locationProvider: any LocationProviding = LocationProvider.shared
|
||||||
@ -358,12 +348,13 @@ final class WildPhotographerReportSubmitViewModel {
|
|||||||
let address = snapshot.address.isEmpty
|
let address = snapshot.address.isEmpty
|
||||||
? String(format: "%.5f, %.5f", snapshot.latitude, snapshot.longitude)
|
? String(format: "%.5f, %.5f", snapshot.latitude, snapshot.longitude)
|
||||||
: snapshot.address
|
: snapshot.address
|
||||||
|
let reportAddress = Self.simplifiedReportLocation(address)
|
||||||
locationSnapshot = HomeLocationSnapshot(
|
locationSnapshot = HomeLocationSnapshot(
|
||||||
latitude: snapshot.latitude,
|
latitude: snapshot.latitude,
|
||||||
longitude: snapshot.longitude,
|
longitude: snapshot.longitude,
|
||||||
address: address
|
address: reportAddress
|
||||||
)
|
)
|
||||||
reportLocation = address
|
reportLocation = reportAddress
|
||||||
locationConfirmed = true
|
locationConfirmed = true
|
||||||
isUpdatingLocation = false
|
isUpdatingLocation = false
|
||||||
notifyStateChange()
|
notifyStateChange()
|
||||||
@ -393,6 +384,40 @@ final class WildPhotographerReportSubmitViewModel {
|
|||||||
return types.filter { seen.insert($0.value).inserted }
|
return types.filter { seen.insert($0.value).inserted }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func simplifiedReportLocation(_ address: String) -> String {
|
||||||
|
let trimmed = address.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !trimmed.isEmpty else { return trimmed }
|
||||||
|
let detail = WildReportLocationParser.split(trimmed).detail
|
||||||
|
let simplified = removeAdministrativePrefix(from: detail)
|
||||||
|
return simplified.isEmpty ? trimmed : simplified
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func removeAdministrativePrefix(from address: String) -> String {
|
||||||
|
var text = address
|
||||||
|
removeLeadingRegion(from: &text, matches: [
|
||||||
|
"北京市", "天津市", "上海市", "重庆市",
|
||||||
|
"内蒙古自治区", "广西壮族自治区", "西藏自治区", "宁夏回族自治区", "新疆维吾尔自治区"
|
||||||
|
])
|
||||||
|
removeLeadingRegion(from: &text, suffixes: ["省"], maxLength: 8)
|
||||||
|
removeLeadingRegion(from: &text, suffixes: ["自治州", "地区", "盟", "市"], maxLength: 12)
|
||||||
|
removeLeadingRegion(from: &text, suffixes: ["自治县", "新区", "区", "县", "旗", "市"], maxLength: 12)
|
||||||
|
return text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func removeLeadingRegion(from text: inout String, matches: [String]) {
|
||||||
|
guard let match = matches.first(where: { text.hasPrefix($0) }) else { return }
|
||||||
|
text.removeFirst(match.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func removeLeadingRegion(from text: inout String, suffixes: [String], maxLength: Int) {
|
||||||
|
guard let match = suffixes.compactMap({ suffix -> String? in
|
||||||
|
guard let range = text.range(of: suffix) else { return nil }
|
||||||
|
let prefix = String(text[..<range.upperBound])
|
||||||
|
return prefix.count <= maxLength ? prefix : nil
|
||||||
|
}).min(by: { $0.count < $1.count }) else { return }
|
||||||
|
text.removeFirst(match.count)
|
||||||
|
}
|
||||||
|
|
||||||
private var evidenceCount: Int {
|
private var evidenceCount: Int {
|
||||||
images.count + videos.count
|
images.count + videos.count
|
||||||
}
|
}
|
||||||
|
|||||||
105
suixinkan/UI/Common/KeyboardAvoidingDialogView.swift
Normal file
105
suixinkan/UI/Common/KeyboardAvoidingDialogView.swift
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
//
|
||||||
|
// KeyboardAvoidingDialogView.swift
|
||||||
|
// suixinkan
|
||||||
|
//
|
||||||
|
|
||||||
|
import SnapKit
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 自定义居中弹窗的键盘避让基类,负责在键盘弹出时同步移动内容卡片。
|
||||||
|
class KeyboardAvoidingDialogView: UIView {
|
||||||
|
|
||||||
|
/// 需要避让键盘的弹窗内容视图,通常是白色卡片容器。
|
||||||
|
var keyboardAvoidingContentView: UIView?
|
||||||
|
|
||||||
|
/// 内容视图相对父视图的 centerY 约束,用于随键盘动画更新偏移。
|
||||||
|
var keyboardAvoidingCenterYConstraint: Constraint?
|
||||||
|
|
||||||
|
/// 内容视图与屏幕顶部、键盘顶部之间保留的最小距离。
|
||||||
|
var keyboardAvoidingMinimumSpacing: CGFloat = AppSpacing.md
|
||||||
|
|
||||||
|
private var keyboardObservers: [NSObjectProtocol] = []
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
unregisterKeyboardObservers()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didMoveToSuperview() {
|
||||||
|
super.didMoveToSuperview()
|
||||||
|
if superview == nil {
|
||||||
|
unregisterKeyboardObservers()
|
||||||
|
} else {
|
||||||
|
registerKeyboardObservers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func registerKeyboardObservers() {
|
||||||
|
unregisterKeyboardObservers()
|
||||||
|
let center = NotificationCenter.default
|
||||||
|
keyboardObservers = [
|
||||||
|
center.addObserver(
|
||||||
|
forName: UIResponder.keyboardWillChangeFrameNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] notification in
|
||||||
|
self?.handleKeyboardNotification(notification)
|
||||||
|
},
|
||||||
|
center.addObserver(
|
||||||
|
forName: UIResponder.keyboardWillHideNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] notification in
|
||||||
|
self?.handleKeyboardNotification(notification)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func unregisterKeyboardObservers() {
|
||||||
|
keyboardObservers.forEach(NotificationCenter.default.removeObserver)
|
||||||
|
keyboardObservers.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleKeyboardNotification(_ notification: Notification) {
|
||||||
|
layoutIfNeeded()
|
||||||
|
guard let userInfo = notification.userInfo else { return }
|
||||||
|
|
||||||
|
let duration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.25
|
||||||
|
let curveRaw = (userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue
|
||||||
|
?? UInt(UIView.AnimationCurve.easeInOut.rawValue)
|
||||||
|
let options = UIView.AnimationOptions(rawValue: curveRaw << 16)
|
||||||
|
|
||||||
|
keyboardAvoidingCenterYConstraint?.update(offset: keyboardAvoidanceOffset(from: userInfo))
|
||||||
|
UIView.animate(withDuration: duration, delay: 0, options: options) {
|
||||||
|
self.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func keyboardAvoidanceOffset(from userInfo: [AnyHashable: Any]) -> CGFloat {
|
||||||
|
guard
|
||||||
|
let contentView = keyboardAvoidingContentView,
|
||||||
|
let keyboardFrameValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
|
||||||
|
else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let keyboardFrame = convert(keyboardFrameValue.cgRectValue, from: nil)
|
||||||
|
guard keyboardFrame.minY < bounds.height else { return 0 }
|
||||||
|
|
||||||
|
let contentHeight = contentView.bounds.height
|
||||||
|
let baseMinY = bounds.midY - contentHeight / 2
|
||||||
|
let baseMaxY = bounds.midY + contentHeight / 2
|
||||||
|
let desiredBottom = keyboardFrame.minY - keyboardAvoidingMinimumSpacing
|
||||||
|
let neededShift = max(0, baseMaxY - desiredBottom)
|
||||||
|
let maxShift = max(0, baseMinY - keyboardAvoidingMinimumSpacing)
|
||||||
|
return -min(neededShift, maxShift)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -630,7 +630,7 @@ final class CooperationAcquirerCell: UITableViewCell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 修改获客员分成比例弹窗,对齐 Android `CommissionRateEditDialog`。
|
/// 修改获客员分成比例弹窗,对齐 Android `CommissionRateEditDialog`。
|
||||||
final class CooperationCommissionRateDialogView: UIView {
|
final class CooperationCommissionRateDialogView: KeyboardAvoidingDialogView {
|
||||||
|
|
||||||
var onCommissionRateChange: ((String) -> Void)?
|
var onCommissionRateChange: ((String) -> Void)?
|
||||||
var onSmsCodeChange: ((String) -> Void)?
|
var onSmsCodeChange: ((String) -> Void)?
|
||||||
@ -654,8 +654,6 @@ final class CooperationCommissionRateDialogView: UIView {
|
|||||||
private let buttonStack = UIStackView()
|
private let buttonStack = UIStackView()
|
||||||
private let cancelButton = UIButton(type: .system)
|
private let cancelButton = UIButton(type: .system)
|
||||||
private let confirmButton = UIButton(type: .system)
|
private let confirmButton = UIButton(type: .system)
|
||||||
private var cardCenterYConstraint: Constraint?
|
|
||||||
private var keyboardObservers: [NSObjectProtocol] = []
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
@ -667,10 +665,6 @@ final class CooperationCommissionRateDialogView: UIView {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
|
||||||
unregisterKeyboardObservers()
|
|
||||||
}
|
|
||||||
|
|
||||||
func apply(commissionRate: String, smsCode: String, phone: String, countdown: Int) {
|
func apply(commissionRate: String, smsCode: String, phone: String, countdown: Int) {
|
||||||
rateField.text = commissionRate
|
rateField.text = commissionRate
|
||||||
smsCodeField.text = smsCode
|
smsCodeField.text = smsCode
|
||||||
@ -690,7 +684,6 @@ final class CooperationCommissionRateDialogView: UIView {
|
|||||||
alpha = 0
|
alpha = 0
|
||||||
parent.addSubview(self)
|
parent.addSubview(self)
|
||||||
layoutIfNeeded()
|
layoutIfNeeded()
|
||||||
registerKeyboardObservers()
|
|
||||||
rateField.focus()
|
rateField.focus()
|
||||||
UIView.animate(withDuration: 0.2) {
|
UIView.animate(withDuration: 0.2) {
|
||||||
self.alpha = 1
|
self.alpha = 1
|
||||||
@ -699,7 +692,6 @@ final class CooperationCommissionRateDialogView: UIView {
|
|||||||
|
|
||||||
func dismiss() {
|
func dismiss() {
|
||||||
endEditing(true)
|
endEditing(true)
|
||||||
unregisterKeyboardObservers()
|
|
||||||
UIView.animate(withDuration: 0.2, animations: {
|
UIView.animate(withDuration: 0.2, animations: {
|
||||||
self.alpha = 0
|
self.alpha = 0
|
||||||
}, completion: { _ in
|
}, completion: { _ in
|
||||||
@ -708,6 +700,8 @@ final class CooperationCommissionRateDialogView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func setupUI() {
|
private func setupUI() {
|
||||||
|
keyboardAvoidingContentView = cardView
|
||||||
|
|
||||||
dimView.backgroundColor = AppColor.overlayScrim
|
dimView.backgroundColor = AppColor.overlayScrim
|
||||||
dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cancelTapped)))
|
dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cancelTapped)))
|
||||||
|
|
||||||
@ -780,7 +774,7 @@ final class CooperationCommissionRateDialogView: UIView {
|
|||||||
}
|
}
|
||||||
cardView.snp.makeConstraints { make in
|
cardView.snp.makeConstraints { make in
|
||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
cardCenterYConstraint = make.centerY.equalToSuperview().constraint
|
keyboardAvoidingCenterYConstraint = make.centerY.equalToSuperview().constraint
|
||||||
}
|
}
|
||||||
titleLabel.snp.makeConstraints { make in
|
titleLabel.snp.makeConstraints { make in
|
||||||
make.top.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
make.top.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
@ -815,67 +809,6 @@ final class CooperationCommissionRateDialogView: UIView {
|
|||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
private func registerKeyboardObservers() {
|
|
||||||
unregisterKeyboardObservers()
|
|
||||||
let center = NotificationCenter.default
|
|
||||||
keyboardObservers = [
|
|
||||||
center.addObserver(
|
|
||||||
forName: UIResponder.keyboardWillChangeFrameNotification,
|
|
||||||
object: nil,
|
|
||||||
queue: .main
|
|
||||||
) { [weak self] notification in
|
|
||||||
self?.handleKeyboardNotification(notification)
|
|
||||||
},
|
|
||||||
center.addObserver(
|
|
||||||
forName: UIResponder.keyboardWillHideNotification,
|
|
||||||
object: nil,
|
|
||||||
queue: .main
|
|
||||||
) { [weak self] notification in
|
|
||||||
self?.handleKeyboardNotification(notification)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
private func unregisterKeyboardObservers() {
|
|
||||||
keyboardObservers.forEach(NotificationCenter.default.removeObserver)
|
|
||||||
keyboardObservers.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleKeyboardNotification(_ notification: Notification) {
|
|
||||||
layoutIfNeeded()
|
|
||||||
guard let userInfo = notification.userInfo else { return }
|
|
||||||
|
|
||||||
let duration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.25
|
|
||||||
let curveRaw = (userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue
|
|
||||||
?? UInt(UIView.AnimationCurve.easeInOut.rawValue)
|
|
||||||
let options = UIView.AnimationOptions(rawValue: curveRaw << 16)
|
|
||||||
|
|
||||||
let offset = keyboardAvoidanceOffset(from: userInfo)
|
|
||||||
cardCenterYConstraint?.update(offset: offset)
|
|
||||||
UIView.animate(withDuration: duration, delay: 0, options: options) {
|
|
||||||
self.layoutIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func keyboardAvoidanceOffset(from userInfo: [AnyHashable: Any]) -> CGFloat {
|
|
||||||
guard
|
|
||||||
let keyboardFrameValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
|
|
||||||
else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
let keyboardFrame = convert(keyboardFrameValue.cgRectValue, from: nil)
|
|
||||||
guard keyboardFrame.minY < bounds.height else { return 0 }
|
|
||||||
|
|
||||||
let cardHeight = cardView.bounds.height
|
|
||||||
let baseMinY = bounds.midY - cardHeight / 2
|
|
||||||
let baseMaxY = bounds.midY + cardHeight / 2
|
|
||||||
let desiredBottom = keyboardFrame.minY - AppSpacing.md
|
|
||||||
let neededShift = max(0, baseMaxY - desiredBottom)
|
|
||||||
let maxShift = max(0, baseMinY - AppSpacing.md)
|
|
||||||
return -min(neededShift, maxShift)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func sendCodeTapped() {
|
@objc private func sendCodeTapped() {
|
||||||
onSendSmsCode?()
|
onSendSmsCode?()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -267,10 +267,26 @@ final class MaterialFormViewController: BaseViewController {
|
|||||||
sheet.addAction(UIAlertAction(title: "从相册选择", style: .default) { [weak self] _ in
|
sheet.addAction(UIAlertAction(title: "从相册选择", style: .default) { [weak self] _ in
|
||||||
self?.presentPhotoPicker()
|
self?.presentPhotoPicker()
|
||||||
})
|
})
|
||||||
|
if target == .material {
|
||||||
|
sheet.addAction(UIAlertAction(title: "从云盘导入", style: .default) { [weak self] _ in
|
||||||
|
self?.openCloudPicker()
|
||||||
|
})
|
||||||
|
}
|
||||||
sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
|
sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||||
present(sheet, animated: true)
|
present(sheet, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func openCloudPicker() {
|
||||||
|
let controller = CloudStoragePickForTaskViewController(
|
||||||
|
importedFileIDs: viewModel.importedCloudFileIDs,
|
||||||
|
allowedFileType: viewModel.mediaType.cloudFileType
|
||||||
|
)
|
||||||
|
controller.onConfirmed = { [weak self] files in
|
||||||
|
self?.viewModel.addCloudMediaFiles(files)
|
||||||
|
}
|
||||||
|
navigationController?.pushViewController(controller, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
private func presentPhotoPicker() {
|
private func presentPhotoPicker() {
|
||||||
var configuration = PHPickerConfiguration(photoLibrary: .shared())
|
var configuration = PHPickerConfiguration(photoLibrary: .shared())
|
||||||
if pickTarget == .cover {
|
if pickTarget == .cover {
|
||||||
|
|||||||
@ -220,7 +220,18 @@ private extension String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 自定义订单退款弹窗视图,对齐 Android `OrderRefundDialog` / `DepositOrderRefundDialog`。
|
/// 自定义订单退款弹窗视图,对齐 Android `OrderRefundDialog` / `DepositOrderRefundDialog`。
|
||||||
final class OrderRefundDialogView: UIView, UITextViewDelegate {
|
final class OrderRefundDialogView: KeyboardAvoidingDialogView, UITextViewDelegate {
|
||||||
|
|
||||||
|
/// 退款弹窗局部布局尺寸,保持内容紧凑但不牺牲基础点击区域。
|
||||||
|
private enum Layout {
|
||||||
|
static let sectionGap: CGFloat = AppSpacing.lg
|
||||||
|
static let formGap: CGFloat = AppSpacing.md
|
||||||
|
static let controlHeight: CGFloat = AppSpacing.minTouchTarget
|
||||||
|
static let amountGroupHeight: CGFloat = 67
|
||||||
|
static let remarkHeight: CGFloat = 88
|
||||||
|
static let actionTop: CGFloat = 14
|
||||||
|
static let bottomInset: CGFloat = AppSpacing.md
|
||||||
|
}
|
||||||
|
|
||||||
/// 退款弹窗展示与校验配置,用于区分普通订单和店铺/定金订单。
|
/// 退款弹窗展示与校验配置,用于区分普通订单和店铺/定金订单。
|
||||||
struct Configuration {
|
struct Configuration {
|
||||||
@ -266,8 +277,6 @@ final class OrderRefundDialogView: UIView, UITextViewDelegate {
|
|||||||
|
|
||||||
private let configuration: Configuration
|
private let configuration: Configuration
|
||||||
private var selectedMode: OrderRefundMode
|
private var selectedMode: OrderRefundMode
|
||||||
private var keyboardObservers: [NSObjectProtocol] = []
|
|
||||||
private var cardCenterYConstraint: Constraint?
|
|
||||||
|
|
||||||
private let dimView = UIView()
|
private let dimView = UIView()
|
||||||
private let cardView = UIView()
|
private let cardView = UIView()
|
||||||
@ -304,16 +313,11 @@ final class OrderRefundDialogView: UIView, UITextViewDelegate {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
|
||||||
unregisterKeyboardObservers()
|
|
||||||
}
|
|
||||||
|
|
||||||
func show(in parent: UIView) {
|
func show(in parent: UIView) {
|
||||||
frame = parent.bounds
|
frame = parent.bounds
|
||||||
autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
alpha = 0
|
alpha = 0
|
||||||
parent.addSubview(self)
|
parent.addSubview(self)
|
||||||
registerKeyboardObservers()
|
|
||||||
UIView.animate(withDuration: 0.2) {
|
UIView.animate(withDuration: 0.2) {
|
||||||
self.alpha = 1
|
self.alpha = 1
|
||||||
}
|
}
|
||||||
@ -321,7 +325,6 @@ final class OrderRefundDialogView: UIView, UITextViewDelegate {
|
|||||||
|
|
||||||
func dismiss() {
|
func dismiss() {
|
||||||
endEditing(true)
|
endEditing(true)
|
||||||
unregisterKeyboardObservers()
|
|
||||||
UIView.animate(withDuration: 0.2, animations: {
|
UIView.animate(withDuration: 0.2, animations: {
|
||||||
self.alpha = 0
|
self.alpha = 0
|
||||||
}, completion: { _ in
|
}, completion: { _ in
|
||||||
@ -330,6 +333,9 @@ final class OrderRefundDialogView: UIView, UITextViewDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func setupUI() {
|
private func setupUI() {
|
||||||
|
keyboardAvoidingContentView = cardView
|
||||||
|
keyboardAvoidingMinimumSpacing = AppSpacing.sm
|
||||||
|
|
||||||
dimView.backgroundColor = UIColor.black.withAlphaComponent(0.2)
|
dimView.backgroundColor = UIColor.black.withAlphaComponent(0.2)
|
||||||
dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cancelTapped)))
|
dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cancelTapped)))
|
||||||
|
|
||||||
@ -422,7 +428,7 @@ final class OrderRefundDialogView: UIView, UITextViewDelegate {
|
|||||||
}
|
}
|
||||||
cardView.snp.makeConstraints { make in
|
cardView.snp.makeConstraints { make in
|
||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
cardCenterYConstraint = make.centerY.equalToSuperview().constraint
|
keyboardAvoidingCenterYConstraint = make.centerY.equalToSuperview().constraint
|
||||||
}
|
}
|
||||||
titleLabel.snp.makeConstraints { make in
|
titleLabel.snp.makeConstraints { make in
|
||||||
make.top.equalToSuperview().offset(AppSpacing.sm)
|
make.top.equalToSuperview().offset(AppSpacing.sm)
|
||||||
@ -442,18 +448,18 @@ final class OrderRefundDialogView: UIView, UITextViewDelegate {
|
|||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
}
|
}
|
||||||
refundModeTitleLabel.snp.makeConstraints { make in
|
refundModeTitleLabel.snp.makeConstraints { make in
|
||||||
make.top.equalTo(orderNumberLabel.snp.bottom).offset(AppSpacing.xl)
|
make.top.equalTo(orderNumberLabel.snp.bottom).offset(Layout.sectionGap)
|
||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
}
|
}
|
||||||
modeButtonStack.snp.makeConstraints { make in
|
modeButtonStack.snp.makeConstraints { make in
|
||||||
make.top.equalTo(refundModeTitleLabel.snp.bottom).offset(AppSpacing.xs)
|
make.top.equalTo(refundModeTitleLabel.snp.bottom).offset(AppSpacing.xs)
|
||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
make.height.equalTo(46)
|
make.height.equalTo(Layout.controlHeight)
|
||||||
}
|
}
|
||||||
amountGroupView.snp.makeConstraints { make in
|
amountGroupView.snp.makeConstraints { make in
|
||||||
make.top.equalTo(modeButtonStack.snp.bottom).offset(AppSpacing.xl)
|
make.top.equalTo(modeButtonStack.snp.bottom).offset(Layout.formGap)
|
||||||
make.leading.trailing.equalToSuperview()
|
make.leading.trailing.equalToSuperview()
|
||||||
make.height.equalTo(73)
|
make.height.equalTo(Layout.amountGroupHeight)
|
||||||
}
|
}
|
||||||
amountTitleLabel.snp.makeConstraints { make in
|
amountTitleLabel.snp.makeConstraints { make in
|
||||||
make.top.leading.trailing.equalToSuperview().inset(UIEdgeInsets(top: 0, left: AppSpacing.md, bottom: 0, right: AppSpacing.md))
|
make.top.leading.trailing.equalToSuperview().inset(UIEdgeInsets(top: 0, left: AppSpacing.md, bottom: 0, right: AppSpacing.md))
|
||||||
@ -461,20 +467,20 @@ final class OrderRefundDialogView: UIView, UITextViewDelegate {
|
|||||||
amountContainerView.snp.makeConstraints { make in
|
amountContainerView.snp.makeConstraints { make in
|
||||||
make.top.equalTo(amountTitleLabel.snp.bottom).offset(AppSpacing.xs)
|
make.top.equalTo(amountTitleLabel.snp.bottom).offset(AppSpacing.xs)
|
||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
make.height.equalTo(46)
|
make.height.equalTo(Layout.controlHeight)
|
||||||
}
|
}
|
||||||
amountField.snp.makeConstraints { make in
|
amountField.snp.makeConstraints { make in
|
||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.sm)
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.sm)
|
||||||
make.centerY.equalToSuperview()
|
make.centerY.equalToSuperview()
|
||||||
}
|
}
|
||||||
remarkTitleLabel.snp.makeConstraints { make in
|
remarkTitleLabel.snp.makeConstraints { make in
|
||||||
make.top.equalTo(amountGroupView.snp.bottom).offset(AppSpacing.xl)
|
make.top.equalTo(amountGroupView.snp.bottom).offset(Layout.formGap)
|
||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
}
|
}
|
||||||
remarkContainerView.snp.makeConstraints { make in
|
remarkContainerView.snp.makeConstraints { make in
|
||||||
make.top.equalTo(remarkTitleLabel.snp.bottom).offset(AppSpacing.xs)
|
make.top.equalTo(remarkTitleLabel.snp.bottom).offset(AppSpacing.xs)
|
||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
make.height.equalTo(104)
|
make.height.equalTo(Layout.remarkHeight)
|
||||||
}
|
}
|
||||||
remarkTextView.snp.makeConstraints { make in
|
remarkTextView.snp.makeConstraints { make in
|
||||||
make.edges.equalToSuperview()
|
make.edges.equalToSuperview()
|
||||||
@ -484,9 +490,9 @@ final class OrderRefundDialogView: UIView, UITextViewDelegate {
|
|||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.sm)
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.sm)
|
||||||
}
|
}
|
||||||
actionRow.snp.makeConstraints { make in
|
actionRow.snp.makeConstraints { make in
|
||||||
make.top.equalTo(remarkContainerView.snp.bottom).offset(20)
|
make.top.equalTo(remarkContainerView.snp.bottom).offset(Layout.actionTop)
|
||||||
make.trailing.equalToSuperview().inset(AppSpacing.md)
|
make.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
make.bottom.equalToSuperview().inset(AppSpacing.lg)
|
make.bottom.equalToSuperview().inset(Layout.bottomInset)
|
||||||
}
|
}
|
||||||
[cancelButton, confirmButton].forEach { button in
|
[cancelButton, confirmButton].forEach { button in
|
||||||
button.snp.makeConstraints { make in
|
button.snp.makeConstraints { make in
|
||||||
@ -521,10 +527,10 @@ final class OrderRefundDialogView: UIView, UITextViewDelegate {
|
|||||||
let amountHidden = selectedMode == .full
|
let amountHidden = selectedMode == .full
|
||||||
amountGroupView.isHidden = amountHidden
|
amountGroupView.isHidden = amountHidden
|
||||||
amountGroupView.snp.updateConstraints { make in
|
amountGroupView.snp.updateConstraints { make in
|
||||||
make.height.equalTo(amountHidden ? 0 : 73)
|
make.height.equalTo(amountHidden ? 0 : Layout.amountGroupHeight)
|
||||||
}
|
}
|
||||||
remarkTitleLabel.snp.remakeConstraints { make in
|
remarkTitleLabel.snp.remakeConstraints { make in
|
||||||
make.top.equalTo((amountHidden ? modeButtonStack : amountGroupView).snp.bottom).offset(AppSpacing.xl)
|
make.top.equalTo((amountHidden ? modeButtonStack : amountGroupView).snp.bottom).offset(Layout.formGap)
|
||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -544,60 +550,6 @@ final class OrderRefundDialogView: UIView, UITextViewDelegate {
|
|||||||
button.setTitleColor(selected ? .white : UIColor(hex: 0x4B5563), for: .normal)
|
button.setTitleColor(selected ? .white : UIColor(hex: 0x4B5563), for: .normal)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func registerKeyboardObservers() {
|
|
||||||
unregisterKeyboardObservers()
|
|
||||||
let center = NotificationCenter.default
|
|
||||||
keyboardObservers = [
|
|
||||||
center.addObserver(
|
|
||||||
forName: UIResponder.keyboardWillChangeFrameNotification,
|
|
||||||
object: nil,
|
|
||||||
queue: .main
|
|
||||||
) { [weak self] notification in
|
|
||||||
self?.handleKeyboardNotification(notification)
|
|
||||||
},
|
|
||||||
center.addObserver(
|
|
||||||
forName: UIResponder.keyboardWillHideNotification,
|
|
||||||
object: nil,
|
|
||||||
queue: .main
|
|
||||||
) { [weak self] notification in
|
|
||||||
self?.handleKeyboardNotification(notification)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
private func unregisterKeyboardObservers() {
|
|
||||||
keyboardObservers.forEach(NotificationCenter.default.removeObserver)
|
|
||||||
keyboardObservers.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleKeyboardNotification(_ notification: Notification) {
|
|
||||||
layoutIfNeeded()
|
|
||||||
guard let userInfo = notification.userInfo else { return }
|
|
||||||
let duration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.25
|
|
||||||
let curveRaw = (userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue
|
|
||||||
?? UInt(UIView.AnimationCurve.easeInOut.rawValue)
|
|
||||||
let options = UIView.AnimationOptions(rawValue: curveRaw << 16)
|
|
||||||
let offset = keyboardAvoidanceOffset(from: userInfo)
|
|
||||||
cardCenterYConstraint?.update(offset: offset)
|
|
||||||
UIView.animate(withDuration: duration, delay: 0, options: options) {
|
|
||||||
self.layoutIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func keyboardAvoidanceOffset(from userInfo: [AnyHashable: Any]) -> CGFloat {
|
|
||||||
guard
|
|
||||||
let keyboardFrameValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
|
|
||||||
else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
let keyboardFrame = convert(keyboardFrameValue.cgRectValue, from: nil)
|
|
||||||
guard keyboardFrame.minY < bounds.height else { return 0 }
|
|
||||||
|
|
||||||
let desiredBottom = keyboardFrame.minY - AppSpacing.sm
|
|
||||||
let overlap = cardView.frame.maxY - desiredBottom
|
|
||||||
return overlap > 0 ? -overlap : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func textViewDidChange(_ textView: UITextView) {
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
if textView.text.count > 255 {
|
if textView.text.count > 255 {
|
||||||
textView.text = String(textView.text.prefix(255))
|
textView.text = String(textView.text.prefix(255))
|
||||||
|
|||||||
@ -17,6 +17,7 @@ final class OrderSourceEntryView: UIControl {
|
|||||||
private let personRow = UIStackView()
|
private let personRow = UIStackView()
|
||||||
private let personNameLabel = UILabel()
|
private let personNameLabel = UILabel()
|
||||||
private let personPhoneLabel = UILabel()
|
private let personPhoneLabel = UILabel()
|
||||||
|
private let personSpacerView = UIView()
|
||||||
private let divider = UIView()
|
private let divider = UIView()
|
||||||
private let leadContentView = OrderSourceLeadContentView()
|
private let leadContentView = OrderSourceLeadContentView()
|
||||||
private var currentImages: [String] = []
|
private var currentImages: [String] = []
|
||||||
@ -83,10 +84,19 @@ final class OrderSourceEntryView: UIControl {
|
|||||||
personRow.spacing = 10
|
personRow.spacing = 10
|
||||||
personNameLabel.font = .systemFont(ofSize: 13, weight: .medium)
|
personNameLabel.font = .systemFont(ofSize: 13, weight: .medium)
|
||||||
personNameLabel.textColor = AppColor.text333
|
personNameLabel.textColor = AppColor.text333
|
||||||
|
personNameLabel.lineBreakMode = .byTruncatingTail
|
||||||
|
personNameLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
personNameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
personPhoneLabel.font = .systemFont(ofSize: 13)
|
personPhoneLabel.font = .systemFont(ofSize: 13)
|
||||||
personPhoneLabel.textColor = AppColor.text666
|
personPhoneLabel.textColor = AppColor.text666
|
||||||
|
personPhoneLabel.lineBreakMode = .byTruncatingTail
|
||||||
|
personPhoneLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
personPhoneLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
personSpacerView.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||||
|
personSpacerView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
personRow.addArrangedSubview(personNameLabel)
|
personRow.addArrangedSubview(personNameLabel)
|
||||||
personRow.addArrangedSubview(personPhoneLabel)
|
personRow.addArrangedSubview(personPhoneLabel)
|
||||||
|
personRow.addArrangedSubview(personSpacerView)
|
||||||
|
|
||||||
divider.backgroundColor = OrderTokens.sourceDivider
|
divider.backgroundColor = OrderTokens.sourceDivider
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,7 @@ final class OrderSourceLeadContentView: UIView {
|
|||||||
private let phoneRow = UIStackView()
|
private let phoneRow = UIStackView()
|
||||||
private let phoneTitleLabel = UILabel()
|
private let phoneTitleLabel = UILabel()
|
||||||
private let phoneValueLabel = UILabel()
|
private let phoneValueLabel = UILabel()
|
||||||
|
private let phoneSpacerView = UIView()
|
||||||
private let remarkLabel = UILabel()
|
private let remarkLabel = UILabel()
|
||||||
private let imageRow = UIStackView()
|
private let imageRow = UIStackView()
|
||||||
private let emptyImageLabel = UILabel()
|
private let emptyImageLabel = UILabel()
|
||||||
@ -91,12 +92,20 @@ final class OrderSourceLeadContentView: UIView {
|
|||||||
phoneTitleLabel.text = "客户手机号"
|
phoneTitleLabel.text = "客户手机号"
|
||||||
phoneTitleLabel.font = .systemFont(ofSize: 12)
|
phoneTitleLabel.font = .systemFont(ofSize: 12)
|
||||||
phoneTitleLabel.textColor = AppColor.text666
|
phoneTitleLabel.textColor = AppColor.text666
|
||||||
|
phoneTitleLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
phoneTitleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
phoneValueLabel.font = .systemFont(ofSize: 13, weight: .medium)
|
phoneValueLabel.font = .systemFont(ofSize: 13, weight: .medium)
|
||||||
|
phoneValueLabel.lineBreakMode = .byTruncatingTail
|
||||||
|
phoneValueLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
phoneValueLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
phoneValueLabel.addGestureRecognizer(
|
phoneValueLabel.addGestureRecognizer(
|
||||||
UITapGestureRecognizer(target: self, action: #selector(phoneTapped))
|
UITapGestureRecognizer(target: self, action: #selector(phoneTapped))
|
||||||
)
|
)
|
||||||
|
phoneSpacerView.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||||
|
phoneSpacerView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
phoneRow.addArrangedSubview(phoneTitleLabel)
|
phoneRow.addArrangedSubview(phoneTitleLabel)
|
||||||
phoneRow.addArrangedSubview(phoneValueLabel)
|
phoneRow.addArrangedSubview(phoneValueLabel)
|
||||||
|
phoneRow.addArrangedSubview(phoneSpacerView)
|
||||||
|
|
||||||
remarkLabel.textColor = AppColor.text333
|
remarkLabel.textColor = AppColor.text333
|
||||||
remarkLabel.lineBreakMode = .byTruncatingTail
|
remarkLabel.lineBreakMode = .byTruncatingTail
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import SnapKit
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
/// 设置收款金额弹窗,对齐 Android `SetAmountDialog`。
|
/// 设置收款金额弹窗,对齐 Android `SetAmountDialog`。
|
||||||
final class PaymentSetAmountDialogView: UIView {
|
final class PaymentSetAmountDialogView: KeyboardAvoidingDialogView {
|
||||||
|
|
||||||
var onConfirm: (() -> Void)?
|
var onConfirm: (() -> Void)?
|
||||||
var onCancel: (() -> Void)?
|
var onCancel: (() -> Void)?
|
||||||
@ -52,6 +52,8 @@ final class PaymentSetAmountDialogView: UIView {
|
|||||||
autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
alpha = 0
|
alpha = 0
|
||||||
parent.addSubview(self)
|
parent.addSubview(self)
|
||||||
|
layoutIfNeeded()
|
||||||
|
amountField.becomeFirstResponder()
|
||||||
UIView.animate(withDuration: 0.2) {
|
UIView.animate(withDuration: 0.2) {
|
||||||
self.alpha = 1
|
self.alpha = 1
|
||||||
}
|
}
|
||||||
@ -67,6 +69,8 @@ final class PaymentSetAmountDialogView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func setupUI() {
|
private func setupUI() {
|
||||||
|
keyboardAvoidingContentView = cardView
|
||||||
|
|
||||||
dimView.backgroundColor = AppColor.overlayScrim
|
dimView.backgroundColor = AppColor.overlayScrim
|
||||||
dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cancelTapped)))
|
dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cancelTapped)))
|
||||||
|
|
||||||
@ -146,7 +150,7 @@ final class PaymentSetAmountDialogView: UIView {
|
|||||||
}
|
}
|
||||||
cardView.snp.makeConstraints { make in
|
cardView.snp.makeConstraints { make in
|
||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
make.centerY.equalToSuperview()
|
keyboardAvoidingCenterYConstraint = make.centerY.equalToSuperview().constraint
|
||||||
}
|
}
|
||||||
titleLabel.snp.makeConstraints { make in
|
titleLabel.snp.makeConstraints { make in
|
||||||
make.top.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
make.top.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
|
|||||||
@ -17,6 +17,11 @@ final class StatisticsPeriodButton: UIButton {
|
|||||||
didSet { updateAppearance() }
|
didSet { updateAppearance() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
layer.cornerRadius = bounds.height / 2
|
||||||
|
}
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
setupUI()
|
setupUI()
|
||||||
@ -29,7 +34,7 @@ final class StatisticsPeriodButton: UIButton {
|
|||||||
|
|
||||||
private func setupUI() {
|
private func setupUI() {
|
||||||
titleLabel?.font = .app(.bodyMedium)
|
titleLabel?.font = .app(.bodyMedium)
|
||||||
layer.cornerRadius = AppRadius.pill
|
layer.cornerCurve = .continuous
|
||||||
clipsToBounds = true
|
clipsToBounds = true
|
||||||
contentEdgeInsets = UIEdgeInsets(top: 6, left: 20, bottom: 6, right: 20)
|
contentEdgeInsets = UIEdgeInsets(top: 6, left: 20, bottom: 6, right: 20)
|
||||||
updateAppearance()
|
updateAppearance()
|
||||||
|
|||||||
@ -23,8 +23,9 @@ final class CloudStoragePickForTaskViewController: BaseViewController {
|
|||||||
private let confirmButton = AppButton(title: "确定")
|
private let confirmButton = AppButton(title: "确定")
|
||||||
private let bottomBar = UIView()
|
private let bottomBar = UIView()
|
||||||
|
|
||||||
init(importedFileIDs: Set<Int> = []) {
|
/// 初始化云盘多选页,可按云盘文件类型限制选择范围。
|
||||||
viewModel = CloudStoragePickForTaskViewModel(importedFileIDs: importedFileIDs)
|
init(importedFileIDs: Set<Int> = [], allowedFileType: Int? = nil) {
|
||||||
|
viewModel = CloudStoragePickForTaskViewModel(importedFileIDs: importedFileIDs, allowedFileType: allowedFileType)
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,6 +294,7 @@ private final class CloudPickCell: UICollectionViewCell {
|
|||||||
imageView.image = nil
|
imageView.image = nil
|
||||||
folderIconView.isHidden = true
|
folderIconView.isHidden = true
|
||||||
badgeView.isHidden = true
|
badgeView.isHidden = true
|
||||||
|
contentView.alpha = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func apply(file: CloudFile, isSelected: Bool, isImported: Bool) {
|
func apply(file: CloudFile, isSelected: Bool, isImported: Bool) {
|
||||||
@ -302,6 +304,7 @@ private final class CloudPickCell: UICollectionViewCell {
|
|||||||
badgeView.isHidden = !isSelected
|
badgeView.isHidden = !isSelected
|
||||||
|
|
||||||
if file.isFolder {
|
if file.isFolder {
|
||||||
|
contentView.alpha = 1
|
||||||
imageView.image = nil
|
imageView.image = nil
|
||||||
folderIconView.isHidden = false
|
folderIconView.isHidden = false
|
||||||
return
|
return
|
||||||
|
|||||||
@ -16,9 +16,11 @@ final class TaskAddViewController: BaseViewController {
|
|||||||
private let ossUploadService = NetworkServices.shared.ossUploadService
|
private let ossUploadService = NetworkServices.shared.ossUploadService
|
||||||
|
|
||||||
private let scrollView = UIScrollView()
|
private let scrollView = UIScrollView()
|
||||||
|
private let formCardView = UIView()
|
||||||
private let contentStack = UIStackView()
|
private let contentStack = UIStackView()
|
||||||
private let mediaSection = TaskAddMediaSectionView()
|
private let mediaSection = TaskAddMediaSectionView()
|
||||||
private let prioritySection = TaskPrioritySectionView()
|
private let prioritySection = TaskPrioritySectionView()
|
||||||
|
private let orderTitleLabel = UILabel()
|
||||||
private let orderButton = TaskOrderSelectButton()
|
private let orderButton = TaskOrderSelectButton()
|
||||||
private let nameField = TaskBorderTextField(placeholder: "请输入任务标题")
|
private let nameField = TaskBorderTextField(placeholder: "请输入任务标题")
|
||||||
private let detailsField = TaskBorderTextView(placeholder: "请输入任务描述(必填)")
|
private let detailsField = TaskBorderTextView(placeholder: "请输入任务描述(必填)")
|
||||||
@ -26,6 +28,7 @@ final class TaskAddViewController: BaseViewController {
|
|||||||
private let bottomBar = UIView()
|
private let bottomBar = UIView()
|
||||||
|
|
||||||
private var remarkDialog: TaskFileRemarkDialogView?
|
private var remarkDialog: TaskFileRemarkDialogView?
|
||||||
|
private var uploadTypeSheet: TaskUploadTypeSheetView?
|
||||||
private var pickingMediaType: Int = 2
|
private var pickingMediaType: Int = 2
|
||||||
private var detailsPlaceholder = "请输入任务描述(必填)"
|
private var detailsPlaceholder = "请输入任务描述(必填)"
|
||||||
|
|
||||||
@ -34,37 +37,56 @@ final class TaskAddViewController: BaseViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func setupUI() {
|
override func setupUI() {
|
||||||
|
view.backgroundColor = AppColor.pageBackground
|
||||||
|
scrollView.backgroundColor = AppColor.pageBackground
|
||||||
|
scrollView.keyboardDismissMode = .interactive
|
||||||
|
|
||||||
|
formCardView.backgroundColor = .white
|
||||||
|
formCardView.layer.cornerRadius = AppRadius.lg
|
||||||
|
|
||||||
contentStack.axis = .vertical
|
contentStack.axis = .vertical
|
||||||
contentStack.spacing = AppSpacing.sm
|
contentStack.spacing = AppSpacing.md
|
||||||
|
|
||||||
|
orderTitleLabel.text = "关联订单(可选)"
|
||||||
|
orderTitleLabel.font = .app(.bodyMedium)
|
||||||
|
orderTitleLabel.textColor = AppColor.textPrimary
|
||||||
|
|
||||||
bottomBar.backgroundColor = .white
|
bottomBar.backgroundColor = .white
|
||||||
view.addSubview(scrollView)
|
view.addSubview(scrollView)
|
||||||
view.addSubview(bottomBar)
|
view.addSubview(bottomBar)
|
||||||
scrollView.addSubview(contentStack)
|
scrollView.addSubview(formCardView)
|
||||||
|
formCardView.addSubview(contentStack)
|
||||||
|
|
||||||
contentStack.addArrangedSubview(mediaSection)
|
contentStack.addArrangedSubview(mediaSection)
|
||||||
contentStack.addArrangedSubview(prioritySection)
|
contentStack.addArrangedSubview(prioritySection)
|
||||||
|
contentStack.addArrangedSubview(orderTitleLabel)
|
||||||
contentStack.addArrangedSubview(orderButton)
|
contentStack.addArrangedSubview(orderButton)
|
||||||
contentStack.addArrangedSubview(nameField)
|
contentStack.addArrangedSubview(nameField)
|
||||||
contentStack.addArrangedSubview(detailsField)
|
contentStack.addArrangedSubview(detailsField)
|
||||||
|
contentStack.setCustomSpacing(AppSpacing.xxs, after: orderTitleLabel)
|
||||||
bottomBar.addSubview(saveButton)
|
bottomBar.addSubview(saveButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func setupConstraints() {
|
override func setupConstraints() {
|
||||||
bottomBar.snp.makeConstraints { make in
|
bottomBar.snp.makeConstraints { make in
|
||||||
make.leading.trailing.bottom.equalToSuperview()
|
make.leading.trailing.bottom.equalToSuperview()
|
||||||
|
make.height.equalTo(80)
|
||||||
}
|
}
|
||||||
saveButton.snp.makeConstraints { make in
|
saveButton.snp.makeConstraints { make in
|
||||||
make.edges.equalToSuperview().inset(AppSpacing.md)
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
}
|
}
|
||||||
scrollView.snp.makeConstraints { make in
|
scrollView.snp.makeConstraints { make in
|
||||||
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
|
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
|
||||||
make.bottom.equalTo(bottomBar.snp.top)
|
make.bottom.equalTo(bottomBar.snp.top)
|
||||||
}
|
}
|
||||||
contentStack.snp.makeConstraints { make in
|
formCardView.snp.makeConstraints { make in
|
||||||
make.edges.equalToSuperview().inset(AppSpacing.md)
|
make.edges.equalToSuperview().inset(AppSpacing.md)
|
||||||
make.width.equalTo(scrollView.snp.width).offset(-AppSpacing.md * 2)
|
make.width.equalTo(scrollView.snp.width).offset(-AppSpacing.md * 2)
|
||||||
}
|
}
|
||||||
|
contentStack.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview().inset(AppSpacing.md)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func bindActions() {
|
override func bindActions() {
|
||||||
@ -91,6 +113,7 @@ final class TaskAddViewController: BaseViewController {
|
|||||||
nameField.textField.addTarget(self, action: #selector(nameChanged), for: .editingChanged)
|
nameField.textField.addTarget(self, action: #selector(nameChanged), for: .editingChanged)
|
||||||
detailsField.textView.delegate = self
|
detailsField.textView.delegate = self
|
||||||
saveButton.addTarget(self, action: #selector(saveTapped), for: .touchUpInside)
|
saveButton.addTarget(self, action: #selector(saveTapped), for: .touchUpInside)
|
||||||
|
applyViewModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
@ -108,11 +131,15 @@ final class TaskAddViewController: BaseViewController {
|
|||||||
if nameField.textField.text != viewModel.taskName {
|
if nameField.textField.text != viewModel.taskName {
|
||||||
nameField.textField.text = viewModel.taskName
|
nameField.textField.text = viewModel.taskName
|
||||||
}
|
}
|
||||||
if detailsField.textView.textColor != AppColor.textTertiary,
|
if viewModel.taskDetails.isEmpty, !detailsField.textView.isFirstResponder {
|
||||||
detailsField.textView.text != viewModel.taskDetails {
|
detailsField.textView.text = detailsPlaceholder
|
||||||
|
detailsField.textView.textColor = AppColor.textTertiary
|
||||||
|
} else if detailsField.textView.text != viewModel.taskDetails {
|
||||||
detailsField.textView.text = viewModel.taskDetails
|
detailsField.textView.text = viewModel.taskDetails
|
||||||
|
detailsField.textView.textColor = AppColor.textPrimary
|
||||||
}
|
}
|
||||||
saveButton.isEnabled = !viewModel.isUploading
|
saveButton.isEnabled = true
|
||||||
|
saveButton.alpha = viewModel.isUploading ? 0.6 : 1
|
||||||
|
|
||||||
if viewModel.showRemarkDialog, let file = viewModel.remarkTargetFile {
|
if viewModel.showRemarkDialog, let file = viewModel.remarkTargetFile {
|
||||||
showRemarkDialog(file: file)
|
showRemarkDialog(file: file)
|
||||||
@ -151,15 +178,30 @@ final class TaskAddViewController: BaseViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func presentLocalImportSheet() {
|
private func presentLocalImportSheet() {
|
||||||
let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
guard uploadTypeSheet == nil else { return }
|
||||||
sheet.addAction(UIAlertAction(title: "图片", style: .default) { [weak self] _ in
|
let sheet = TaskUploadTypeSheetView(frame: view.bounds)
|
||||||
|
sheet.onChooseImage = { [weak self] in
|
||||||
|
self?.hideUploadTypeSheet()
|
||||||
self?.presentMediaPicker(isImage: true)
|
self?.presentMediaPicker(isImage: true)
|
||||||
})
|
}
|
||||||
sheet.addAction(UIAlertAction(title: "视频", style: .default) { [weak self] _ in
|
sheet.onChooseVideo = { [weak self] in
|
||||||
|
self?.hideUploadTypeSheet()
|
||||||
self?.presentMediaPicker(isImage: false)
|
self?.presentMediaPicker(isImage: false)
|
||||||
})
|
}
|
||||||
sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
|
sheet.onCancel = { [weak self] in
|
||||||
present(sheet, animated: true)
|
self?.hideUploadTypeSheet()
|
||||||
|
}
|
||||||
|
uploadTypeSheet = sheet
|
||||||
|
view.addSubview(sheet)
|
||||||
|
sheet.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||||
|
sheet.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hideUploadTypeSheet() {
|
||||||
|
uploadTypeSheet?.dismiss { [weak self] in
|
||||||
|
self?.uploadTypeSheet?.removeFromSuperview()
|
||||||
|
self?.uploadTypeSheet = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentMediaPicker(isImage: Bool) {
|
private func presentMediaPicker(isImage: Bool) {
|
||||||
@ -257,7 +299,6 @@ final class TaskAddViewController: BaseViewController {
|
|||||||
self?.viewModel.updateUploadProgress(uploadTaskId: uploadTaskId, progress: progress)
|
self?.viewModel.updateUploadProgress(uploadTaskId: uploadTaskId, progress: progress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try await taskAPI.registerUploadedFileURL(scenicId: scenicId, fileURL: fileURL, folderId: 0)
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.viewModel.markUploadSucceeded(
|
self.viewModel.markUploadSucceeded(
|
||||||
uploadTaskId: uploadTaskId,
|
uploadTaskId: uploadTaskId,
|
||||||
|
|||||||
@ -17,16 +17,17 @@ final class TaskAddMediaSectionView: UIView {
|
|||||||
|
|
||||||
private var files: [TaskFileItem] = []
|
private var files: [TaskFileItem] = []
|
||||||
private var collectionHeightConstraint: Constraint?
|
private var collectionHeightConstraint: Constraint?
|
||||||
|
private let gridColumns = 3
|
||||||
|
private let gridSpacing: CGFloat = 8
|
||||||
|
|
||||||
private enum Section { case main }
|
private enum Section { case main }
|
||||||
private enum Item: Hashable { case add; case file(String) }
|
private enum Item: Hashable { case add; case file(String) }
|
||||||
|
|
||||||
private lazy var collectionView: UICollectionView = {
|
private lazy var collectionView: UICollectionView = {
|
||||||
let layout = UICollectionViewCompositionalLayout { _, environment in
|
let layout = UICollectionViewCompositionalLayout { [weak self] _, environment in
|
||||||
let columns = 3
|
let columns = self?.gridColumns ?? 3
|
||||||
let spacing: CGFloat = 8
|
let spacing = self?.gridSpacing ?? 8
|
||||||
let inset = AppSpacing.md * 2
|
let availableWidth = environment.container.effectiveContentSize.width
|
||||||
let availableWidth = environment.container.effectiveContentSize.width - inset
|
|
||||||
let totalSpacing = spacing * CGFloat(columns - 1)
|
let totalSpacing = spacing * CGFloat(columns - 1)
|
||||||
let itemWidth = max(0, (availableWidth - totalSpacing) / CGFloat(columns))
|
let itemWidth = max(0, (availableWidth - totalSpacing) / CGFloat(columns))
|
||||||
let itemHeight = itemWidth / 1.77
|
let itemHeight = itemWidth / 1.77
|
||||||
@ -83,8 +84,7 @@ final class TaskAddMediaSectionView: UIView {
|
|||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
backgroundColor = .white
|
backgroundColor = .clear
|
||||||
layer.cornerRadius = AppRadius.md
|
|
||||||
|
|
||||||
titleLabel.text = "添加图片"
|
titleLabel.text = "添加图片"
|
||||||
titleLabel.font = .app(.bodyMedium)
|
titleLabel.font = .app(.bodyMedium)
|
||||||
@ -93,11 +93,13 @@ final class TaskAddMediaSectionView: UIView {
|
|||||||
tipsLabel.text = "(点击图片或视频可添加备注)"
|
tipsLabel.text = "(点击图片或视频可添加备注)"
|
||||||
tipsLabel.font = .app(.caption)
|
tipsLabel.font = .app(.caption)
|
||||||
tipsLabel.textColor = AppColor.textPrimary
|
tipsLabel.textColor = AppColor.textPrimary
|
||||||
|
tipsLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
|
|
||||||
cloudButton.setTitle("云盘导入", for: .normal)
|
cloudButton.setTitle("云盘导入", for: .normal)
|
||||||
cloudButton.setTitleColor(AppColor.primary, for: .normal)
|
cloudButton.setTitleColor(AppColor.primary, for: .normal)
|
||||||
cloudButton.titleLabel?.font = .app(.caption)
|
cloudButton.titleLabel?.font = .app(.caption)
|
||||||
cloudButton.addTarget(self, action: #selector(cloudTapped), for: .touchUpInside)
|
cloudButton.addTarget(self, action: #selector(cloudTapped), for: .touchUpInside)
|
||||||
|
cloudButton.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
|
||||||
collectionView.register(TaskAddMediaCell.self, forCellWithReuseIdentifier: TaskAddMediaCell.reuseIdentifier)
|
collectionView.register(TaskAddMediaCell.self, forCellWithReuseIdentifier: TaskAddMediaCell.reuseIdentifier)
|
||||||
collectionView.register(TaskAddAddMediaCell.self, forCellWithReuseIdentifier: TaskAddAddMediaCell.reuseIdentifier)
|
collectionView.register(TaskAddAddMediaCell.self, forCellWithReuseIdentifier: TaskAddAddMediaCell.reuseIdentifier)
|
||||||
@ -109,20 +111,20 @@ final class TaskAddMediaSectionView: UIView {
|
|||||||
addSubview(collectionView)
|
addSubview(collectionView)
|
||||||
|
|
||||||
titleLabel.snp.makeConstraints { make in
|
titleLabel.snp.makeConstraints { make in
|
||||||
make.top.leading.equalToSuperview().offset(AppSpacing.md)
|
make.top.leading.equalToSuperview()
|
||||||
}
|
}
|
||||||
tipsLabel.snp.makeConstraints { make in
|
tipsLabel.snp.makeConstraints { make in
|
||||||
make.leading.equalTo(titleLabel.snp.trailing).offset(AppSpacing.xxs)
|
make.leading.equalTo(titleLabel.snp.trailing).offset(AppSpacing.xxs)
|
||||||
make.centerY.equalTo(titleLabel)
|
make.centerY.equalTo(titleLabel)
|
||||||
|
make.trailing.lessThanOrEqualTo(cloudButton.snp.leading).offset(-AppSpacing.xs)
|
||||||
}
|
}
|
||||||
cloudButton.snp.makeConstraints { make in
|
cloudButton.snp.makeConstraints { make in
|
||||||
make.trailing.equalToSuperview().inset(AppSpacing.md)
|
make.trailing.equalToSuperview()
|
||||||
make.centerY.equalTo(titleLabel)
|
make.centerY.equalTo(titleLabel)
|
||||||
}
|
}
|
||||||
collectionView.snp.makeConstraints { make in
|
collectionView.snp.makeConstraints { make in
|
||||||
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.sm)
|
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.sm)
|
||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
make.leading.trailing.bottom.equalToSuperview()
|
||||||
make.bottom.equalToSuperview().inset(AppSpacing.md)
|
|
||||||
collectionHeightConstraint = make.height.equalTo(88).constraint
|
collectionHeightConstraint = make.height.equalTo(88).constraint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -138,9 +140,22 @@ final class TaskAddMediaSectionView: UIView {
|
|||||||
snapshot.appendSections([.main])
|
snapshot.appendSections([.main])
|
||||||
snapshot.appendItems(files.map { Item.file($0.id) } + [.add])
|
snapshot.appendItems(files.map { Item.file($0.id) } + [.add])
|
||||||
dataSource.apply(snapshot, animatingDifferences: false)
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
collectionView.layoutIfNeeded()
|
updateCollectionHeight(itemCount: files.count + 1)
|
||||||
let height = max(88, collectionView.collectionViewLayout.collectionViewContentSize.height)
|
}
|
||||||
collectionHeightConstraint?.update(offset: height)
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
updateCollectionHeight(itemCount: files.count + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateCollectionHeight(itemCount: Int) {
|
||||||
|
let availableWidth = collectionView.bounds.width
|
||||||
|
guard availableWidth > 0 else { return }
|
||||||
|
let itemWidth = max(0, (availableWidth - gridSpacing * CGFloat(gridColumns - 1)) / CGFloat(gridColumns))
|
||||||
|
let itemHeight = itemWidth / 1.77
|
||||||
|
let rows = max(1, Int(ceil(Double(itemCount) / Double(gridColumns))))
|
||||||
|
let height = itemHeight * CGFloat(rows) + gridSpacing * CGFloat(rows - 1)
|
||||||
|
collectionHeightConstraint?.update(offset: max(88, height))
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func cloudTapped() {
|
@objc private func cloudTapped() {
|
||||||
@ -161,6 +176,54 @@ extension TaskAddMediaSectionView: UICollectionViewDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 圆形上传进度视图,对齐 Android 上传素材 cell 的进度表现。
|
||||||
|
final class TaskCircularProgressView: UIView {
|
||||||
|
|
||||||
|
private let trackLayer = CAShapeLayer()
|
||||||
|
private let progressLayer = CAShapeLayer()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
isHidden = true
|
||||||
|
layer.addSublayer(trackLayer)
|
||||||
|
layer.addSublayer(progressLayer)
|
||||||
|
trackLayer.fillColor = UIColor.clear.cgColor
|
||||||
|
trackLayer.strokeColor = UIColor.white.withAlphaComponent(0.35).cgColor
|
||||||
|
trackLayer.lineWidth = 3
|
||||||
|
progressLayer.fillColor = UIColor.clear.cgColor
|
||||||
|
progressLayer.strokeColor = UIColor.white.cgColor
|
||||||
|
progressLayer.lineCap = .round
|
||||||
|
progressLayer.lineWidth = 3
|
||||||
|
progressLayer.strokeEnd = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
let radius = min(bounds.width, bounds.height) / 2 - progressLayer.lineWidth / 2
|
||||||
|
let path = UIBezierPath(
|
||||||
|
arcCenter: CGPoint(x: bounds.midX, y: bounds.midY),
|
||||||
|
radius: radius,
|
||||||
|
startAngle: -.pi / 2,
|
||||||
|
endAngle: .pi * 1.5,
|
||||||
|
clockwise: true
|
||||||
|
)
|
||||||
|
trackLayer.frame = bounds
|
||||||
|
progressLayer.frame = bounds
|
||||||
|
trackLayer.path = path.cgPath
|
||||||
|
progressLayer.path = path.cgPath
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置 0...1 的上传进度。
|
||||||
|
func setProgress(_ progress: CGFloat) {
|
||||||
|
progressLayer.strokeEnd = min(1, max(0, progress))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 素材缩略图 cell。
|
/// 素材缩略图 cell。
|
||||||
final class TaskAddMediaCell: UICollectionViewCell {
|
final class TaskAddMediaCell: UICollectionViewCell {
|
||||||
|
|
||||||
@ -170,9 +233,11 @@ final class TaskAddMediaCell: UICollectionViewCell {
|
|||||||
|
|
||||||
private let imageView = UIImageView()
|
private let imageView = UIImageView()
|
||||||
private let overlayView = UIView()
|
private let overlayView = UIView()
|
||||||
|
private let progressRingView = TaskCircularProgressView()
|
||||||
private let progressLabel = UILabel()
|
private let progressLabel = UILabel()
|
||||||
private let deleteButton = UIButton(type: .system)
|
private let deleteButton = UIButton(type: .system)
|
||||||
private let playIconView = UIImageView()
|
private let playIconView = UIImageView()
|
||||||
|
private let sourceBadgeLabel = UILabel()
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
@ -183,7 +248,7 @@ final class TaskAddMediaCell: UICollectionViewCell {
|
|||||||
imageView.contentMode = .scaleAspectFill
|
imageView.contentMode = .scaleAspectFill
|
||||||
imageView.clipsToBounds = true
|
imageView.clipsToBounds = true
|
||||||
|
|
||||||
overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.35)
|
overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.2)
|
||||||
overlayView.isHidden = true
|
overlayView.isHidden = true
|
||||||
|
|
||||||
progressLabel.font = .app(.captionMedium)
|
progressLabel.font = .app(.captionMedium)
|
||||||
@ -198,22 +263,45 @@ final class TaskAddMediaCell: UICollectionViewCell {
|
|||||||
playIconView.tintColor = .white
|
playIconView.tintColor = .white
|
||||||
playIconView.isHidden = true
|
playIconView.isHidden = true
|
||||||
|
|
||||||
|
sourceBadgeLabel.font = .app(.caption)
|
||||||
|
sourceBadgeLabel.textColor = .white
|
||||||
|
sourceBadgeLabel.textAlignment = .center
|
||||||
|
sourceBadgeLabel.backgroundColor = UIColor.black.withAlphaComponent(0.5)
|
||||||
|
sourceBadgeLabel.layer.cornerRadius = AppRadius.xs
|
||||||
|
sourceBadgeLabel.clipsToBounds = true
|
||||||
|
sourceBadgeLabel.isHidden = true
|
||||||
|
|
||||||
contentView.addSubview(imageView)
|
contentView.addSubview(imageView)
|
||||||
contentView.addSubview(overlayView)
|
contentView.addSubview(overlayView)
|
||||||
|
contentView.addSubview(sourceBadgeLabel)
|
||||||
contentView.addSubview(playIconView)
|
contentView.addSubview(playIconView)
|
||||||
|
contentView.addSubview(progressRingView)
|
||||||
contentView.addSubview(progressLabel)
|
contentView.addSubview(progressLabel)
|
||||||
contentView.addSubview(deleteButton)
|
contentView.addSubview(deleteButton)
|
||||||
|
|
||||||
imageView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
imageView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||||
overlayView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
overlayView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||||
|
sourceBadgeLabel.snp.makeConstraints { make in
|
||||||
|
make.leading.bottom.equalToSuperview().inset(AppSpacing.xxs)
|
||||||
|
make.height.equalTo(18)
|
||||||
|
make.width.greaterThanOrEqualTo(34)
|
||||||
|
}
|
||||||
playIconView.snp.makeConstraints { make in
|
playIconView.snp.makeConstraints { make in
|
||||||
make.center.equalToSuperview()
|
make.center.equalToSuperview()
|
||||||
|
make.width.height.equalTo(24)
|
||||||
|
}
|
||||||
|
progressRingView.snp.makeConstraints { make in
|
||||||
|
make.centerX.equalToSuperview()
|
||||||
|
make.centerY.equalToSuperview().offset(-10)
|
||||||
make.width.height.equalTo(28)
|
make.width.height.equalTo(28)
|
||||||
}
|
}
|
||||||
progressLabel.snp.makeConstraints { make in make.center.equalToSuperview() }
|
progressLabel.snp.makeConstraints { make in
|
||||||
|
make.top.equalTo(progressRingView.snp.bottom).offset(2)
|
||||||
|
make.centerX.equalToSuperview()
|
||||||
|
}
|
||||||
deleteButton.snp.makeConstraints { make in
|
deleteButton.snp.makeConstraints { make in
|
||||||
make.top.trailing.equalToSuperview().inset(AppSpacing.xxs)
|
make.top.trailing.equalToSuperview().inset(AppSpacing.xxs)
|
||||||
make.width.height.equalTo(22)
|
make.width.height.equalTo(20)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,6 +315,7 @@ final class TaskAddMediaCell: UICollectionViewCell {
|
|||||||
onDelete = nil
|
onDelete = nil
|
||||||
imageView.kf.cancelDownloadTask()
|
imageView.kf.cancelDownloadTask()
|
||||||
imageView.image = nil
|
imageView.image = nil
|
||||||
|
progressRingView.setProgress(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func apply(file: TaskFileItem) {
|
func apply(file: TaskFileItem) {
|
||||||
@ -237,12 +326,18 @@ final class TaskAddMediaCell: UICollectionViewCell {
|
|||||||
imageView.image = UIImage(systemName: "photo")
|
imageView.image = UIImage(systemName: "photo")
|
||||||
imageView.tintColor = AppColor.textTertiary
|
imageView.tintColor = AppColor.textTertiary
|
||||||
}
|
}
|
||||||
playIconView.isHidden = file.fileType != 1
|
playIconView.isHidden = file.fileType != 1 || file.isUploading
|
||||||
|
deleteButton.isHidden = file.isUploading
|
||||||
|
sourceBadgeLabel.isHidden = file.isUploading
|
||||||
|
sourceBadgeLabel.text = file.sourceBadgeText
|
||||||
if file.isUploading {
|
if file.isUploading {
|
||||||
overlayView.isHidden = false
|
overlayView.isHidden = false
|
||||||
|
progressRingView.isHidden = false
|
||||||
|
progressRingView.setProgress(CGFloat(file.uploadProgress) / 100)
|
||||||
progressLabel.text = "\(file.uploadProgress)%"
|
progressLabel.text = "\(file.uploadProgress)%"
|
||||||
} else {
|
} else {
|
||||||
overlayView.isHidden = true
|
overlayView.isHidden = true
|
||||||
|
progressRingView.isHidden = true
|
||||||
progressLabel.text = nil
|
progressLabel.text = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -302,11 +397,14 @@ final class TaskPrioritySectionView: UIView {
|
|||||||
private let twoHourButton = UIButton(type: .system)
|
private let twoHourButton = UIButton(type: .system)
|
||||||
private let customButton = UIButton(type: .system)
|
private let customButton = UIButton(type: .system)
|
||||||
private let customField = UITextField()
|
private let customField = UITextField()
|
||||||
|
private let customConfirmButton = UIButton(type: .system)
|
||||||
|
private let optionRow = UIStackView()
|
||||||
|
private let customRow = UIStackView()
|
||||||
|
private let stackView = UIStackView()
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
backgroundColor = .white
|
backgroundColor = .clear
|
||||||
layer.cornerRadius = AppRadius.md
|
|
||||||
|
|
||||||
titleLabel.text = "优先级"
|
titleLabel.text = "优先级"
|
||||||
titleLabel.font = .app(.bodyMedium)
|
titleLabel.font = .app(.bodyMedium)
|
||||||
@ -324,32 +422,54 @@ final class TaskPrioritySectionView: UIView {
|
|||||||
customField.font = .app(.body)
|
customField.font = .app(.body)
|
||||||
customField.textColor = AppColor.textPrimary
|
customField.textColor = AppColor.textPrimary
|
||||||
customField.keyboardType = .numberPad
|
customField.keyboardType = .numberPad
|
||||||
customField.borderStyle = .roundedRect
|
customField.backgroundColor = .white
|
||||||
customField.isHidden = true
|
customField.layer.cornerRadius = AppRadius.sm
|
||||||
|
customField.layer.borderWidth = 1
|
||||||
|
customField.layer.borderColor = AppColor.border.cgColor
|
||||||
|
customField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: AppSpacing.sm, height: 1))
|
||||||
|
customField.leftViewMode = .always
|
||||||
customField.addTarget(self, action: #selector(customFieldChanged), for: .editingChanged)
|
customField.addTarget(self, action: #selector(customFieldChanged), for: .editingChanged)
|
||||||
|
|
||||||
let row = UIStackView(arrangedSubviews: [noneButton, twoHourButton, customButton])
|
customConfirmButton.setTitle("确定", for: .normal)
|
||||||
row.axis = .horizontal
|
customConfirmButton.setTitleColor(.white, for: .normal)
|
||||||
row.spacing = AppSpacing.xs
|
customConfirmButton.titleLabel?.font = .app(.bodyMedium)
|
||||||
row.distribution = .fillEqually
|
customConfirmButton.backgroundColor = AppColor.primary
|
||||||
|
customConfirmButton.layer.cornerRadius = AppRadius.xs
|
||||||
|
customConfirmButton.addTarget(self, action: #selector(customConfirmTapped), for: .touchUpInside)
|
||||||
|
|
||||||
addSubview(titleLabel)
|
optionRow.addArrangedSubview(noneButton)
|
||||||
addSubview(row)
|
optionRow.addArrangedSubview(twoHourButton)
|
||||||
addSubview(customField)
|
optionRow.addArrangedSubview(customButton)
|
||||||
|
optionRow.axis = .horizontal
|
||||||
|
optionRow.spacing = AppSpacing.xs
|
||||||
|
optionRow.distribution = .fillEqually
|
||||||
|
|
||||||
titleLabel.snp.makeConstraints { make in
|
customRow.addArrangedSubview(customField)
|
||||||
make.top.leading.equalToSuperview().offset(AppSpacing.md)
|
customRow.addArrangedSubview(customConfirmButton)
|
||||||
|
customRow.axis = .horizontal
|
||||||
|
customRow.spacing = AppSpacing.xs
|
||||||
|
customRow.alignment = .fill
|
||||||
|
customRow.isHidden = true
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(titleLabel)
|
||||||
|
stackView.addArrangedSubview(optionRow)
|
||||||
|
stackView.addArrangedSubview(customRow)
|
||||||
|
stackView.axis = .vertical
|
||||||
|
stackView.spacing = AppSpacing.xs
|
||||||
|
|
||||||
|
addSubview(stackView)
|
||||||
|
|
||||||
|
stackView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
}
|
}
|
||||||
row.snp.makeConstraints { make in
|
optionRow.snp.makeConstraints { make in
|
||||||
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.sm)
|
|
||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
|
||||||
make.height.equalTo(42)
|
make.height.equalTo(42)
|
||||||
}
|
}
|
||||||
customField.snp.makeConstraints { make in
|
customField.snp.makeConstraints { make in
|
||||||
make.top.equalTo(row.snp.bottom).offset(AppSpacing.sm)
|
make.height.equalTo(46)
|
||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
}
|
||||||
make.bottom.equalToSuperview().inset(AppSpacing.md)
|
customConfirmButton.snp.makeConstraints { make in
|
||||||
make.height.equalTo(40)
|
make.width.equalTo(72)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,7 +483,7 @@ final class TaskPrioritySectionView: UIView {
|
|||||||
styleOption(noneButton, selected: urgentHour == 0)
|
styleOption(noneButton, selected: urgentHour == 0)
|
||||||
styleOption(twoHourButton, selected: urgentHour == 2)
|
styleOption(twoHourButton, selected: urgentHour == 2)
|
||||||
styleOption(customButton, selected: isCustom)
|
styleOption(customButton, selected: isCustom)
|
||||||
customField.isHidden = !isCustom
|
customRow.isHidden = !isCustom
|
||||||
if isCustom {
|
if isCustom {
|
||||||
customField.text = "\(urgentHour)"
|
customField.text = "\(urgentHour)"
|
||||||
} else {
|
} else {
|
||||||
@ -392,6 +512,9 @@ final class TaskPrioritySectionView: UIView {
|
|||||||
@objc private func customFieldChanged() {
|
@objc private func customFieldChanged() {
|
||||||
onCustomHourTextChange?(customField.text ?? "")
|
onCustomHourTextChange?(customField.text ?? "")
|
||||||
}
|
}
|
||||||
|
@objc private func customConfirmTapped() {
|
||||||
|
onCustomHourTextChange?(customField.text ?? "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 关联订单选择按钮。
|
/// 关联订单选择按钮。
|
||||||
@ -443,7 +566,7 @@ final class TaskOrderSelectButton: UIControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 任务素材备注弹窗。
|
/// 任务素材备注弹窗。
|
||||||
final class TaskFileRemarkDialogView: UIView {
|
final class TaskFileRemarkDialogView: KeyboardAvoidingDialogView {
|
||||||
|
|
||||||
var onCancel: (() -> Void)?
|
var onCancel: (() -> Void)?
|
||||||
var onConfirm: (() -> Void)?
|
var onConfirm: (() -> Void)?
|
||||||
@ -451,31 +574,46 @@ final class TaskFileRemarkDialogView: UIView {
|
|||||||
private let containerView = UIView()
|
private let containerView = UIView()
|
||||||
private let titleLabel = UILabel()
|
private let titleLabel = UILabel()
|
||||||
private let previewView = UIImageView()
|
private let previewView = UIImageView()
|
||||||
|
private let remarkTitleLabel = UILabel()
|
||||||
private let textView = UITextView()
|
private let textView = UITextView()
|
||||||
|
private let placeholderLabel = UILabel()
|
||||||
private let countLabel = UILabel()
|
private let countLabel = UILabel()
|
||||||
private let cancelButton = UIButton(type: .system)
|
private let cancelButton = UIButton(type: .system)
|
||||||
private let confirmButton = UIButton(type: .system)
|
private let confirmButton = UIButton(type: .system)
|
||||||
|
private let buttonRow = UIStackView()
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
backgroundColor = AppColor.overlayScrim
|
backgroundColor = AppColor.overlayScrim
|
||||||
|
keyboardAvoidingContentView = containerView
|
||||||
|
|
||||||
containerView.backgroundColor = .white
|
containerView.backgroundColor = .white
|
||||||
containerView.layer.cornerRadius = AppRadius.md
|
containerView.layer.cornerRadius = AppRadius.lg
|
||||||
|
|
||||||
titleLabel.text = "备注信息"
|
titleLabel.text = "请填写剪辑备注"
|
||||||
titleLabel.font = .app(.title)
|
titleLabel.font = .app(.title)
|
||||||
|
titleLabel.textColor = AppColor.textPrimary
|
||||||
|
|
||||||
previewView.contentMode = .scaleAspectFill
|
previewView.contentMode = .scaleAspectFill
|
||||||
previewView.clipsToBounds = true
|
previewView.clipsToBounds = true
|
||||||
previewView.layer.cornerRadius = AppRadius.sm
|
previewView.layer.cornerRadius = AppRadius.lg
|
||||||
previewView.backgroundColor = AppColor.inputBackground
|
previewView.backgroundColor = AppColor.inputBackground
|
||||||
|
|
||||||
|
remarkTitleLabel.text = "备注信息"
|
||||||
|
remarkTitleLabel.font = .app(.caption)
|
||||||
|
remarkTitleLabel.textColor = AppColor.textPrimary
|
||||||
|
|
||||||
textView.font = .app(.body)
|
textView.font = .app(.body)
|
||||||
|
textView.textColor = AppColor.textPrimary
|
||||||
textView.layer.borderWidth = 1
|
textView.layer.borderWidth = 1
|
||||||
textView.layer.borderColor = AppColor.border.cgColor
|
textView.layer.borderColor = AppColor.border.cgColor
|
||||||
textView.layer.cornerRadius = AppRadius.sm
|
textView.layer.cornerRadius = AppRadius.sm
|
||||||
textView.delegate = self
|
textView.delegate = self
|
||||||
|
textView.textContainerInset = UIEdgeInsets(top: AppSpacing.sm, left: AppSpacing.xs, bottom: AppSpacing.sm, right: AppSpacing.xs)
|
||||||
|
|
||||||
|
placeholderLabel.text = "请输入备注(最多50字)"
|
||||||
|
placeholderLabel.font = .app(.body)
|
||||||
|
placeholderLabel.textColor = AppColor.textTertiary
|
||||||
|
|
||||||
countLabel.font = .app(.caption)
|
countLabel.font = .app(.caption)
|
||||||
countLabel.textColor = AppColor.textTertiary
|
countLabel.textColor = AppColor.textTertiary
|
||||||
@ -493,16 +631,25 @@ final class TaskFileRemarkDialogView: UIView {
|
|||||||
confirmButton.layer.cornerRadius = AppRadius.xs
|
confirmButton.layer.cornerRadius = AppRadius.xs
|
||||||
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
|
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
|
||||||
|
|
||||||
|
buttonRow.axis = .horizontal
|
||||||
|
buttonRow.spacing = AppSpacing.xs
|
||||||
|
buttonRow.alignment = .center
|
||||||
|
buttonRow.distribution = .fill
|
||||||
|
buttonRow.addArrangedSubview(cancelButton)
|
||||||
|
buttonRow.addArrangedSubview(confirmButton)
|
||||||
|
|
||||||
addSubview(containerView)
|
addSubview(containerView)
|
||||||
containerView.addSubview(titleLabel)
|
containerView.addSubview(titleLabel)
|
||||||
containerView.addSubview(previewView)
|
containerView.addSubview(previewView)
|
||||||
containerView.addSubview(textView)
|
containerView.addSubview(remarkTitleLabel)
|
||||||
containerView.addSubview(countLabel)
|
containerView.addSubview(countLabel)
|
||||||
containerView.addSubview(cancelButton)
|
containerView.addSubview(textView)
|
||||||
containerView.addSubview(confirmButton)
|
textView.addSubview(placeholderLabel)
|
||||||
|
containerView.addSubview(buttonRow)
|
||||||
|
|
||||||
containerView.snp.makeConstraints { make in
|
containerView.snp.makeConstraints { make in
|
||||||
make.center.equalToSuperview()
|
make.centerX.equalToSuperview()
|
||||||
|
keyboardAvoidingCenterYConstraint = make.centerY.equalToSuperview().constraint
|
||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.lg)
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.lg)
|
||||||
}
|
}
|
||||||
titleLabel.snp.makeConstraints { make in
|
titleLabel.snp.makeConstraints { make in
|
||||||
@ -511,26 +658,35 @@ final class TaskFileRemarkDialogView: UIView {
|
|||||||
previewView.snp.makeConstraints { make in
|
previewView.snp.makeConstraints { make in
|
||||||
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.sm)
|
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.sm)
|
||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
make.height.equalTo(160)
|
make.height.equalTo(previewView.snp.width)
|
||||||
|
}
|
||||||
|
remarkTitleLabel.snp.makeConstraints { make in
|
||||||
|
make.top.equalTo(previewView.snp.bottom).offset(AppSpacing.md)
|
||||||
|
make.leading.equalToSuperview().offset(AppSpacing.md)
|
||||||
|
}
|
||||||
|
countLabel.snp.makeConstraints { make in
|
||||||
|
make.centerY.equalTo(remarkTitleLabel)
|
||||||
|
make.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
}
|
}
|
||||||
textView.snp.makeConstraints { make in
|
textView.snp.makeConstraints { make in
|
||||||
make.top.equalTo(previewView.snp.bottom).offset(AppSpacing.sm)
|
make.top.equalTo(remarkTitleLabel.snp.bottom).offset(AppSpacing.xs)
|
||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
make.height.equalTo(80)
|
make.height.equalTo(80)
|
||||||
}
|
}
|
||||||
countLabel.snp.makeConstraints { make in
|
placeholderLabel.snp.makeConstraints { make in
|
||||||
make.top.equalTo(textView.snp.bottom).offset(AppSpacing.xxs)
|
make.top.equalToSuperview().offset(AppSpacing.sm)
|
||||||
make.trailing.equalTo(textView)
|
make.leading.equalToSuperview().offset(AppSpacing.sm + 3)
|
||||||
|
make.trailing.lessThanOrEqualToSuperview().inset(AppSpacing.sm)
|
||||||
|
}
|
||||||
|
buttonRow.snp.makeConstraints { make in
|
||||||
|
make.top.equalTo(textView.snp.bottom).offset(AppSpacing.md)
|
||||||
|
make.trailing.bottom.equalToSuperview().inset(AppSpacing.md)
|
||||||
}
|
}
|
||||||
cancelButton.snp.makeConstraints { make in
|
cancelButton.snp.makeConstraints { make in
|
||||||
make.top.equalTo(countLabel.snp.bottom).offset(AppSpacing.md)
|
|
||||||
make.leading.bottom.equalToSuperview().inset(AppSpacing.md)
|
|
||||||
make.height.equalTo(36)
|
make.height.equalTo(36)
|
||||||
make.width.equalTo(72)
|
make.width.equalTo(72)
|
||||||
}
|
}
|
||||||
confirmButton.snp.makeConstraints { make in
|
confirmButton.snp.makeConstraints { make in
|
||||||
make.centerY.equalTo(cancelButton)
|
|
||||||
make.trailing.equalToSuperview().inset(AppSpacing.md)
|
|
||||||
make.height.equalTo(36)
|
make.height.equalTo(36)
|
||||||
make.width.equalTo(72)
|
make.width.equalTo(72)
|
||||||
}
|
}
|
||||||
@ -544,6 +700,7 @@ final class TaskFileRemarkDialogView: UIView {
|
|||||||
func apply(file: TaskFileItem, text: String) {
|
func apply(file: TaskFileItem, text: String) {
|
||||||
textView.text = text
|
textView.text = text
|
||||||
countLabel.text = "\(text.count)/50"
|
countLabel.text = "\(text.count)/50"
|
||||||
|
placeholderLabel.isHidden = !text.isEmpty
|
||||||
let urlString = file.previewURLString
|
let urlString = file.previewURLString
|
||||||
if let url = URL(string: urlString), !urlString.isEmpty {
|
if let url = URL(string: urlString), !urlString.isEmpty {
|
||||||
previewView.kf.setImage(with: url)
|
previewView.kf.setImage(with: url)
|
||||||
@ -565,6 +722,7 @@ extension TaskFileRemarkDialogView: UITextViewDelegate {
|
|||||||
if textView.text.count > 50 {
|
if textView.text.count > 50 {
|
||||||
textView.text = String(textView.text.prefix(50))
|
textView.text = String(textView.text.prefix(50))
|
||||||
}
|
}
|
||||||
|
placeholderLabel.isHidden = !textView.text.isEmpty
|
||||||
countLabel.text = "\(textView.text.count)/50"
|
countLabel.text = "\(textView.text.count)/50"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -625,3 +783,177 @@ final class TaskBorderTextView: UIView {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 任务上传类型底部弹层,对齐 Android `AddFileBottomModalForTask`。
|
||||||
|
final class TaskUploadTypeSheetView: UIView {
|
||||||
|
|
||||||
|
var onChooseImage: (() -> Void)?
|
||||||
|
var onChooseVideo: (() -> Void)?
|
||||||
|
var onCancel: (() -> Void)?
|
||||||
|
|
||||||
|
private let dimmingView = UIView()
|
||||||
|
private let panelView = UIView()
|
||||||
|
private let titleLabel = UILabel()
|
||||||
|
private let imageItem = TaskUploadTypeItemView(
|
||||||
|
title: "上传图片",
|
||||||
|
image: UIImage(named: "icon_cloud_storage_add_photo") ?? UIImage(systemName: "photo")
|
||||||
|
)
|
||||||
|
private let videoItem = TaskUploadTypeItemView(
|
||||||
|
title: "上传视频",
|
||||||
|
image: UIImage(named: "icon_cloud_storage_add_video") ?? UIImage(systemName: "video")
|
||||||
|
)
|
||||||
|
private let dividerView = UIView()
|
||||||
|
private let cancelButton = UIButton(type: .system)
|
||||||
|
private var panelBottomConstraint: Constraint?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
backgroundColor = .clear
|
||||||
|
|
||||||
|
dimmingView.backgroundColor = AppColor.overlayScrim
|
||||||
|
dimmingView.alpha = 0
|
||||||
|
let tap = UITapGestureRecognizer(target: self, action: #selector(cancelTapped))
|
||||||
|
dimmingView.addGestureRecognizer(tap)
|
||||||
|
|
||||||
|
panelView.backgroundColor = .white
|
||||||
|
panelView.layer.cornerRadius = AppRadius.sheetTop
|
||||||
|
panelView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||||
|
panelView.clipsToBounds = true
|
||||||
|
|
||||||
|
titleLabel.text = "选择上传类型"
|
||||||
|
titleLabel.font = .app(.bodyMedium)
|
||||||
|
titleLabel.textColor = AppColor.textPrimary
|
||||||
|
titleLabel.textAlignment = .center
|
||||||
|
|
||||||
|
imageItem.addTarget(self, action: #selector(imageTapped), for: .touchUpInside)
|
||||||
|
videoItem.addTarget(self, action: #selector(videoTapped), for: .touchUpInside)
|
||||||
|
|
||||||
|
dividerView.backgroundColor = AppColor.border
|
||||||
|
|
||||||
|
cancelButton.setTitle("取消", for: .normal)
|
||||||
|
cancelButton.titleLabel?.font = .app(.bodyMedium)
|
||||||
|
cancelButton.setTitleColor(UIColor(hex: 0x4B5563), for: .normal)
|
||||||
|
cancelButton.backgroundColor = AppColor.inputBackground
|
||||||
|
cancelButton.layer.cornerRadius = AppRadius.lg
|
||||||
|
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
|
||||||
|
|
||||||
|
addSubview(dimmingView)
|
||||||
|
addSubview(panelView)
|
||||||
|
panelView.addSubview(titleLabel)
|
||||||
|
panelView.addSubview(imageItem)
|
||||||
|
panelView.addSubview(videoItem)
|
||||||
|
panelView.addSubview(dividerView)
|
||||||
|
panelView.addSubview(cancelButton)
|
||||||
|
|
||||||
|
dimmingView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||||
|
panelView.snp.makeConstraints { make in
|
||||||
|
make.leading.trailing.equalToSuperview()
|
||||||
|
panelBottomConstraint = make.bottom.equalToSuperview().offset(260).constraint
|
||||||
|
}
|
||||||
|
titleLabel.snp.makeConstraints { make in
|
||||||
|
make.top.equalToSuperview().offset(AppSpacing.md)
|
||||||
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
|
}
|
||||||
|
imageItem.snp.makeConstraints { make in
|
||||||
|
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.md)
|
||||||
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
|
make.height.equalTo(58)
|
||||||
|
}
|
||||||
|
videoItem.snp.makeConstraints { make in
|
||||||
|
make.top.equalTo(imageItem.snp.bottom).offset(AppSpacing.sm)
|
||||||
|
make.leading.trailing.equalTo(imageItem)
|
||||||
|
make.height.equalTo(imageItem)
|
||||||
|
}
|
||||||
|
dividerView.snp.makeConstraints { make in
|
||||||
|
make.top.equalTo(videoItem.snp.bottom).offset(AppSpacing.md)
|
||||||
|
make.leading.trailing.equalToSuperview()
|
||||||
|
make.height.equalTo(0.5)
|
||||||
|
}
|
||||||
|
cancelButton.snp.makeConstraints { make in
|
||||||
|
make.top.equalTo(dividerView.snp.bottom).offset(AppSpacing.md)
|
||||||
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
|
make.height.equalTo(48)
|
||||||
|
make.bottom.equalTo(safeAreaLayoutGuide).inset(AppSpacing.md)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 展示底部弹层。
|
||||||
|
func show() {
|
||||||
|
layoutIfNeeded()
|
||||||
|
panelBottomConstraint?.update(offset: 0)
|
||||||
|
UIView.animate(withDuration: 0.22, delay: 0, options: [.curveEaseOut]) {
|
||||||
|
self.dimmingView.alpha = 1
|
||||||
|
self.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 关闭底部弹层。
|
||||||
|
func dismiss(completion: (() -> Void)? = nil) {
|
||||||
|
panelBottomConstraint?.update(offset: panelView.bounds.height)
|
||||||
|
UIView.animate(withDuration: 0.18, delay: 0, options: [.curveEaseIn]) {
|
||||||
|
self.dimmingView.alpha = 0
|
||||||
|
self.layoutIfNeeded()
|
||||||
|
} completion: { _ in
|
||||||
|
completion?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func imageTapped() {
|
||||||
|
onChooseImage?()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func videoTapped() {
|
||||||
|
onChooseVideo?()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func cancelTapped() {
|
||||||
|
onCancel?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 上传类型弹层内的单个选项。
|
||||||
|
final class TaskUploadTypeItemView: UIControl {
|
||||||
|
|
||||||
|
private let iconView = UIImageView()
|
||||||
|
private let titleLabel = UILabel()
|
||||||
|
|
||||||
|
init(title: String, image: UIImage?) {
|
||||||
|
super.init(frame: .zero)
|
||||||
|
backgroundColor = .white
|
||||||
|
layer.cornerRadius = AppRadius.sm
|
||||||
|
layer.borderWidth = 1
|
||||||
|
layer.borderColor = AppColor.inputBackground.cgColor
|
||||||
|
|
||||||
|
iconView.image = image
|
||||||
|
iconView.contentMode = .scaleAspectFit
|
||||||
|
iconView.tintColor = AppColor.primary
|
||||||
|
|
||||||
|
titleLabel.text = title
|
||||||
|
titleLabel.font = .app(.bodyMedium)
|
||||||
|
titleLabel.textColor = .black
|
||||||
|
|
||||||
|
addSubview(iconView)
|
||||||
|
addSubview(titleLabel)
|
||||||
|
|
||||||
|
iconView.snp.makeConstraints { make in
|
||||||
|
make.leading.equalToSuperview().offset(AppSpacing.md)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.width.height.equalTo(32)
|
||||||
|
}
|
||||||
|
titleLabel.snp.makeConstraints { make in
|
||||||
|
make.leading.equalTo(iconView.snp.trailing).offset(AppSpacing.sm)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.trailing.lessThanOrEqualToSuperview().inset(AppSpacing.md)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -156,7 +156,7 @@ final class WildPhotographerReportListViewController: BaseViewController {
|
|||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.hideLoading()
|
self.hideLoading()
|
||||||
guard let payload else { return }
|
guard let payload else { return }
|
||||||
WeChatShareService.shareMiniProgram(payload) { [weak self] result in
|
WeChatShareService.shareMiniProgram(payload, previewImage: WildReportShareCover.image) { [weak self] result in
|
||||||
self?.showWildReportShareResult(result)
|
self?.showWildReportShareResult(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -181,6 +181,13 @@ extension WildPhotographerReportListViewController: UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 举报摄影师微信分享封面资源。
|
||||||
|
private enum WildReportShareCover {
|
||||||
|
static var image: UIImage? {
|
||||||
|
UIImage(named: "report_share_cover")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 我的举报筛选栏,使用四段等宽胶囊按钮展示状态过滤。
|
/// 我的举报筛选栏,使用四段等宽胶囊按钮展示状态过滤。
|
||||||
private final class WildReportFilterTabBarView: UIView {
|
private final class WildReportFilterTabBarView: UIView {
|
||||||
var onSelect: ((WildReportListFilter) -> Void)?
|
var onSelect: ((WildReportListFilter) -> Void)?
|
||||||
@ -1228,7 +1235,7 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
|
|||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.hideLoading()
|
self.hideLoading()
|
||||||
guard let payload else { return }
|
guard let payload else { return }
|
||||||
WeChatShareService.shareMiniProgram(payload) { [weak self] result in
|
WeChatShareService.shareMiniProgram(payload, previewImage: WildReportShareCover.image) { [weak self] result in
|
||||||
self?.showWildReportShareResult(result)
|
self?.showWildReportShareResult(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -330,9 +330,7 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
|
|||||||
let title = WildReportUI.label(viewModel.locationTitleText, font: .systemFont(ofSize: 15, weight: .semibold), color: AppColor.textPrimary, lines: 2)
|
let title = WildReportUI.label(viewModel.locationTitleText, font: .systemFont(ofSize: 15, weight: .semibold), color: AppColor.textPrimary, lines: 2)
|
||||||
title.adjustsFontSizeToFitWidth = true
|
title.adjustsFontSizeToFitWidth = true
|
||||||
title.minimumScaleFactor = 0.78
|
title.minimumScaleFactor = 0.78
|
||||||
let subtitle = WildReportUI.label(viewModel.locationSubtitleText, font: .systemFont(ofSize: 12), color: AppColor.textSecondary, lines: 0)
|
|
||||||
textStack.addArrangedSubview(title)
|
textStack.addArrangedSubview(title)
|
||||||
textStack.addArrangedSubview(subtitle)
|
|
||||||
|
|
||||||
let button = UIButton(type: .system)
|
let button = UIButton(type: .system)
|
||||||
button.setTitle(viewModel.isUpdatingLocation ? "定位中" : "更新定位", for: .normal)
|
button.setTitle(viewModel.isUpdatingLocation ? "定位中" : "更新定位", for: .normal)
|
||||||
|
|||||||
@ -73,6 +73,25 @@ final class CloudStoragePickForTaskViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(Set(viewModel.selectedFileList.map(\.id)), Set([1, 2]))
|
XCTAssertEqual(Set(viewModel.selectedFileList.map(\.id)), Set([1, 2]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testAllowedFileTypeRejectsMismatchedMedia() async throws {
|
||||||
|
let listJSON = try makeListJSON(files: [
|
||||||
|
makeFileJSON(id: 1, name: "视频", type: 1),
|
||||||
|
makeFileJSON(id: 2, name: "图片", type: 2),
|
||||||
|
])
|
||||||
|
let api = makeAPI(responses: [listJSON])
|
||||||
|
let viewModel = CloudStoragePickForTaskViewModel(allowedFileType: 2)
|
||||||
|
var message: String?
|
||||||
|
viewModel.onShowMessage = { message = $0 }
|
||||||
|
await viewModel.refresh(api: api)
|
||||||
|
|
||||||
|
await viewModel.handleItemTap(viewModel.files[0], api: api)
|
||||||
|
await viewModel.handleItemTap(viewModel.files[1], api: api)
|
||||||
|
|
||||||
|
XCTAssertEqual(message, "请选择图片素材")
|
||||||
|
XCTAssertFalse(viewModel.isSelected(1))
|
||||||
|
XCTAssertTrue(viewModel.isSelected(2))
|
||||||
|
}
|
||||||
|
|
||||||
private func makeAPI(responses: [Data]) -> TaskAPI {
|
private func makeAPI(responses: [Data]) -> TaskAPI {
|
||||||
TaskAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: responses)))
|
TaskAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: responses)))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -118,6 +118,45 @@ final class MaterialManagementViewModelTests: XCTestCase {
|
|||||||
XCTAssertNil(api.lastUploadRequest?.id)
|
XCTAssertNil(api.lastUploadRequest?.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testCloudMediaImportBuildsMaterialRequestWithoutUploadingAgain() async {
|
||||||
|
let api = MockMaterialAPI()
|
||||||
|
api.spotResponse = ScenicSpotListResponse(list: [ScenicSpotItem(id: 9, name: "打卡点")], total: 1)
|
||||||
|
let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
|
||||||
|
var didSucceed = false
|
||||||
|
viewModel.onSubmitSuccess = { didSucceed = true }
|
||||||
|
|
||||||
|
await viewModel.loadInitial(api: api)
|
||||||
|
viewModel.updateMaterialName("云盘素材")
|
||||||
|
viewModel.selectScenicSpot(ScenicSpotItem(id: 9, name: "打卡点"))
|
||||||
|
viewModel.addCloudMediaFiles([
|
||||||
|
CloudFile(id: 31, fileUrl: "https://cdn/cloud.jpg", coverUrl: "https://cdn/cover-thumb.jpg", name: "cloud.jpg", type: 2, fileSize: 88),
|
||||||
|
])
|
||||||
|
await viewModel.setCoverImage(makeUploadItem(fileName: "cover.jpg"), uploader: MockMaterialUploader(urls: ["https://cdn/cover.jpg"]))
|
||||||
|
await viewModel.submit(api: api)
|
||||||
|
|
||||||
|
XCTAssertTrue(didSucceed)
|
||||||
|
XCTAssertEqual(viewModel.importedCloudFileIDs, Set([31]))
|
||||||
|
XCTAssertEqual(api.lastUploadRequest?.mediaList.first?.originalName, "cloud.jpg")
|
||||||
|
XCTAssertEqual(api.lastUploadRequest?.mediaList.first?.ossUrl, "https://cdn/cloud.jpg")
|
||||||
|
XCTAssertEqual(api.lastUploadRequest?.mediaList.first?.size, 88)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCloudMediaImportFiltersByCurrentMaterialTypeAndDeduplicates() {
|
||||||
|
let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
|
||||||
|
var messages: [String] = []
|
||||||
|
viewModel.onShowMessage = { messages.append($0) }
|
||||||
|
|
||||||
|
viewModel.addCloudMediaFiles([
|
||||||
|
CloudFile(id: 1, fileUrl: "https://cdn/video.mp4", name: "video.mp4", type: 1),
|
||||||
|
CloudFile(id: 2, fileUrl: "https://cdn/image.jpg", name: "image.jpg", type: 2),
|
||||||
|
CloudFile(id: 2, fileUrl: "https://cdn/image.jpg", name: "image.jpg", type: 2),
|
||||||
|
])
|
||||||
|
|
||||||
|
XCTAssertEqual(viewModel.uploadedMediaList.map(\.ossUrl), ["https://cdn/image.jpg"])
|
||||||
|
XCTAssertEqual(viewModel.importedCloudFileIDs, Set([2]))
|
||||||
|
XCTAssertTrue(messages.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
func testUploadDialogStaysHiddenAfterCompletionProgressCallback() async {
|
func testUploadDialogStaysHiddenAfterCompletionProgressCallback() async {
|
||||||
let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
|
let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
|
||||||
let uploader = MockMaterialUploader(urls: ["https://cdn/a.jpg"])
|
let uploader = MockMaterialUploader(urls: ["https://cdn/a.jpg"])
|
||||||
|
|||||||
@ -30,6 +30,32 @@ final class TaskAddViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(message, "请填写任务名称!")
|
XCTAssertEqual(message, "请填写任务名称!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testTextInputsAreTrimmedToAndroidLimits() {
|
||||||
|
let viewModel = TaskAddViewModel(appStore: appStore)
|
||||||
|
|
||||||
|
viewModel.updateTaskName(String(repeating: "任", count: 25))
|
||||||
|
viewModel.updateTaskDetails(String(repeating: "描", count: 210))
|
||||||
|
viewModel.addCloudFiles([CloudFile(id: 1, name: "A", type: 2)])
|
||||||
|
viewModel.presentRemarkDialog(for: viewModel.selectedFiles[0].id)
|
||||||
|
viewModel.updateRemarkDialogText(String(repeating: "注", count: 60))
|
||||||
|
|
||||||
|
XCTAssertEqual(viewModel.taskName.count, 20)
|
||||||
|
XCTAssertEqual(viewModel.taskDetails.count, 200)
|
||||||
|
XCTAssertEqual(viewModel.remarkDialogText.count, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUploadingStateBlocksSubmitButKeepsButtonActionUseful() async {
|
||||||
|
let viewModel = TaskAddViewModel(appStore: appStore)
|
||||||
|
viewModel.updateTaskName("任务")
|
||||||
|
viewModel.addUploadPlaceholder(fileName: "a.jpg", fileType: 2, uploadTaskId: "task1")
|
||||||
|
var message: String?
|
||||||
|
viewModel.onShowMessage = { message = $0 }
|
||||||
|
|
||||||
|
await viewModel.validateAndSubmit(api: TaskAPI(client: makeClient()))
|
||||||
|
|
||||||
|
XCTAssertEqual(message, "文件正在上传中,请稍后添加!")
|
||||||
|
}
|
||||||
|
|
||||||
func testBuildAddTaskRequestSplitsCloudAndUploadFiles() {
|
func testBuildAddTaskRequestSplitsCloudAndUploadFiles() {
|
||||||
let viewModel = TaskAddViewModel(appStore: appStore)
|
let viewModel = TaskAddViewModel(appStore: appStore)
|
||||||
viewModel.updateTaskName("任务A")
|
viewModel.updateTaskName("任务A")
|
||||||
@ -50,6 +76,14 @@ final class TaskAddViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(request.uploadFile[0].fileUrl, "https://cdn/a.jpg")
|
XCTAssertEqual(request.uploadFile[0].fileUrl, "https://cdn/a.jpg")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testTaskFileSourceBadgeTextMatchesAndroidLabels() {
|
||||||
|
let cloudItem = TaskFileItem(source: .cloud, cloudFile: CloudFile(id: 1, name: "云文件", type: 2))
|
||||||
|
let uploadItem = TaskFileItem(source: .upload, uploadFileName: "a.jpg", uploadFileType: 2)
|
||||||
|
|
||||||
|
XCTAssertEqual(cloudItem.sourceBadgeText, "云盘")
|
||||||
|
XCTAssertEqual(uploadItem.sourceBadgeText, "本地")
|
||||||
|
}
|
||||||
|
|
||||||
func testAddCloudFilesDedupesAndAutoRemarkSingleFile() {
|
func testAddCloudFilesDedupesAndAutoRemarkSingleFile() {
|
||||||
let viewModel = TaskAddViewModel(appStore: appStore)
|
let viewModel = TaskAddViewModel(appStore: appStore)
|
||||||
viewModel.addCloudFiles([CloudFile(id: 1, name: "A", type: 2)])
|
viewModel.addCloudFiles([CloudFile(id: 1, name: "A", type: 2)])
|
||||||
@ -74,8 +108,12 @@ final class TaskAddViewModelTests: XCTestCase {
|
|||||||
viewModel.resetAllState()
|
viewModel.resetAllState()
|
||||||
|
|
||||||
XCTAssertEqual(viewModel.taskName, "")
|
XCTAssertEqual(viewModel.taskName, "")
|
||||||
|
XCTAssertEqual(viewModel.taskDetails, "")
|
||||||
XCTAssertNil(viewModel.selectedOrderNumber)
|
XCTAssertNil(viewModel.selectedOrderNumber)
|
||||||
|
XCTAssertEqual(viewModel.urgentHour, 0)
|
||||||
XCTAssertTrue(viewModel.selectedFiles.isEmpty)
|
XCTAssertTrue(viewModel.selectedFiles.isEmpty)
|
||||||
|
XCTAssertFalse(viewModel.isUploading)
|
||||||
|
XCTAssertFalse(viewModel.showSuccessDialog)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeClient() -> APIClient {
|
private func makeClient() -> APIClient {
|
||||||
|
|||||||
@ -701,6 +701,20 @@ final class WildPhotographerReportTests: XCTestCase {
|
|||||||
XCTAssertEqual(messages.last, URLError(.badServerResponse).localizedDescription)
|
XCTAssertEqual(messages.last, URLError(.badServerResponse).localizedDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testSubmitLocationOmitsProvinceCityDistrict() {
|
||||||
|
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||||||
|
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||||||
|
|
||||||
|
viewModel.applyResolvedLocation(HomeLocationSnapshot(
|
||||||
|
latitude: 32.40,
|
||||||
|
longitude: 119.41,
|
||||||
|
address: "江苏省扬州市邗江区瘦西湖路"
|
||||||
|
))
|
||||||
|
|
||||||
|
XCTAssertEqual(viewModel.locationTitleText, "当前定位:瘦西湖路")
|
||||||
|
XCTAssertEqual(viewModel.reportLocation, "瘦西湖路")
|
||||||
|
}
|
||||||
|
|
||||||
func testSubmitFailsWithoutEvidence() async {
|
func testSubmitFailsWithoutEvidence() async {
|
||||||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||||||
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||||||
@ -766,7 +780,7 @@ final class WildPhotographerReportTests: XCTestCase {
|
|||||||
viewModel.addImages([try makeAttachment(kind: .image, fileName: "scene.jpg")])
|
viewModel.addImages([try makeAttachment(kind: .image, fileName: "scene.jpg")])
|
||||||
viewModel.addPaymentImages([try makeAttachment(kind: .payment, fileName: "payment.jpg")])
|
viewModel.addPaymentImages([try makeAttachment(kind: .payment, fileName: "payment.jpg")])
|
||||||
viewModel.updateDescription("测试说明")
|
viewModel.updateDescription("测试说明")
|
||||||
viewModel.applyResolvedLocation(HomeLocationSnapshot(latitude: 30.12, longitude: 120.34, address: "测试景区 · 测试点"))
|
viewModel.applyResolvedLocation(HomeLocationSnapshot(latitude: 30.12, longitude: 120.34, address: "江苏省扬州市邗江区瘦西湖路"))
|
||||||
|
|
||||||
await viewModel.submitReport(api: api, uploader: uploader, scenicId: 9, scenicName: "测试景区")
|
await viewModel.submitReport(api: api, uploader: uploader, scenicId: 9, scenicName: "测试景区")
|
||||||
|
|
||||||
@ -779,10 +793,13 @@ final class WildPhotographerReportTests: XCTestCase {
|
|||||||
XCTAssertEqual(api.submitRequests.first?.reportType, 2)
|
XCTAssertEqual(api.submitRequests.first?.reportType, 2)
|
||||||
XCTAssertEqual(api.submitRequests.first?.lat, 30.12)
|
XCTAssertEqual(api.submitRequests.first?.lat, 30.12)
|
||||||
XCTAssertEqual(api.submitRequests.first?.lng, 120.34)
|
XCTAssertEqual(api.submitRequests.first?.lng, 120.34)
|
||||||
|
XCTAssertEqual(api.submitRequests.first?.locationName, "测试景区")
|
||||||
|
XCTAssertEqual(api.submitRequests.first?.locationAddress, "瘦西湖路")
|
||||||
XCTAssertEqual(api.submitRequests.first?.evidences.count, 1)
|
XCTAssertEqual(api.submitRequests.first?.evidences.count, 1)
|
||||||
XCTAssertEqual(api.submitRequests.first?.evidences.first?.fileType, 1)
|
XCTAssertEqual(api.submitRequests.first?.evidences.first?.fileType, 1)
|
||||||
XCTAssertEqual(api.submitRequests.first?.paymentScreenshots.count, 1)
|
XCTAssertEqual(api.submitRequests.first?.paymentScreenshots.count, 1)
|
||||||
XCTAssertNil(api.submitRequests.first?.storeUserId)
|
XCTAssertNil(api.submitRequests.first?.storeUserId)
|
||||||
|
XCTAssertEqual(viewModel.submitResult?.record.location, "瘦西湖路")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPaymentScreenshotsDoNotReplaceRequiredEvidence() async throws {
|
func testPaymentScreenshotsDoNotReplaceRequiredEvidence() async throws {
|
||||||
|
|||||||
Reference in New Issue
Block a user