Add OSS upload and Kingfisher image support

This commit is contained in:
2026-06-22 16:30:05 +08:00
parent 484566d27e
commit 0b83a73509
20 changed files with 1399 additions and 134 deletions

View File

@ -5,13 +5,19 @@
// Created by Codex on 2026/6/22.
//
import PhotosUI
import SwiftUI
import UIKit
///
struct RealNameAuthView: View {
@Environment(AccountContext.self) private var accountContext
@Environment(ProfileAPI.self) private var profileAPI
@Environment(OSSUploadService.self) private var ossUploadService
@Environment(ToastCenter.self) private var toastCenter
@State private var viewModel = RealNameAuthViewModel()
@State private var pickedFrontItem: PhotosPickerItem?
@State private var pickedBackItem: PhotosPickerItem?
var body: some View {
ScrollView {
@ -39,6 +45,12 @@ struct RealNameAuthView: View {
.background(Color.white.opacity(0.35))
}
}
.onChange(of: pickedFrontItem) { _, item in
Task { await prepareIdentityImage(from: item, side: .front) }
}
.onChange(of: pickedBackItem) { _, item in
Task { await prepareIdentityImage(from: item, side: .back) }
}
}
private var auditStatusPanel: some View {
@ -69,16 +81,28 @@ struct RealNameAuthView: View {
private var identityCard: some View {
VStack(alignment: .leading, spacing: 14) {
identityImageSection(title: "身份证国徽面", url: viewModel.backUrl)
identityImageSection(title: "身份证人像", url: viewModel.frontUrl)
identityImageSection(
title: "身份证国徽",
side: .back,
pickedItem: $pickedBackItem,
pendingData: viewModel.pendingBackImageData,
remoteURL: viewModel.backUrl,
progress: viewModel.backUploadProgress
)
identityImageSection(
title: "身份证人像面",
side: .front,
pickedItem: $pickedFrontItem,
pendingData: viewModel.pendingFrontImageData,
remoteURL: viewModel.frontUrl,
progress: viewModel.frontUploadProgress
)
formField("姓名", text: realNameBinding, placeholder: "请输入姓名")
formField("身份证号码", text: idCardNoBinding, placeholder: "请输入身份证号码")
validitySection
if shouldShowSmsSection {
smsSection
}
urlField("身份证人像面图片 URL", text: frontURLBinding)
urlField("身份证国徽面图片 URL", text: backURLBinding)
submitButton
statusBanner
}
@ -88,12 +112,23 @@ struct RealNameAuthView: View {
}
///
private func identityImageSection(title: String, url: String) -> some View {
private func identityImageSection(
title: String,
side: RealNameImageSide,
pickedItem: Binding<PhotosPickerItem?>,
pendingData: Data?,
remoteURL: String,
progress: Int?
) -> some View {
VStack(alignment: .leading, spacing: 10) {
Text(title)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(Color(hex: 0x222222))
imagePreview(url: url)
PhotosPicker(selection: pickedItem, matching: .images) {
imagePreview(side: side, pendingData: pendingData, remoteURL: remoteURL, progress: progress)
}
.buttonStyle(.plain)
.disabled(viewModel.submitting || viewModel.info?.verified == true)
}
}
@ -237,21 +272,27 @@ struct RealNameAuthView: View {
}
///
private func imagePreview(url: String) -> some View {
private func imagePreview(side: RealNameImageSide, pendingData: Data?, remoteURL: String, progress: Int?) -> some View {
ZStack {
if let imageURL = URL(string: url.trimmingCharacters(in: .whitespacesAndNewlines)), !url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
AsyncImage(url: imageURL) { phase in
switch phase {
case .success(let image):
image.resizable().scaledToFit()
case .empty:
ProgressView().tint(AppDesign.primary)
default:
placeholderImageContent
}
}
if let pendingData, let image = UIImage(data: pendingData) {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
placeholderImageContent
RemoteImage(urlString: remoteURL, contentMode: .fit) {
placeholderImageContent(side: side)
}
}
if let progress {
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(.black.opacity(0.32))
Text("\(progress)%")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(.white)
}
}
}
.frame(maxWidth: .infinity)
@ -264,12 +305,18 @@ struct RealNameAuthView: View {
)
}
private var placeholderImageContent: some View {
///
private func placeholderImageContent(side: RealNameImageSide) -> some View {
ZStack {
Color(hex: 0xF3F6FA)
Image(systemName: "photo")
.font(.system(size: 26, weight: .semibold))
.foregroundStyle(Color(hex: 0x8A94A6))
VStack(spacing: 8) {
Image(systemName: "photo.badge.plus")
.font(.system(size: 26, weight: .semibold))
.foregroundStyle(Color(hex: 0x8A94A6))
Text(side == .front ? "选择身份证人像面" : "选择身份证国徽面")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(Color(hex: 0x8A94A6))
}
}
}
@ -286,11 +333,6 @@ struct RealNameAuthView: View {
}
}
/// URL
private func urlField(_ title: String, text: Binding<String>) -> some View {
formField(title, text: text, placeholder: "OSS 上传接入前可先填写图片 URL")
}
///
private func dateField(selection: Binding<Date>) -> some View {
DatePicker("", selection: selection, displayedComponents: .date)
@ -327,14 +369,6 @@ struct RealNameAuthView: View {
Binding(get: { viewModel.smsCode }, set: { viewModel.smsCode = $0 })
}
private var frontURLBinding: Binding<String> {
Binding(get: { viewModel.frontUrl }, set: { viewModel.frontUrl = $0 })
}
private var backURLBinding: Binding<String> {
Binding(get: { viewModel.backUrl }, set: { viewModel.backUrl = $0 })
}
private var startDateBinding: Binding<Date> {
Binding(get: { viewModel.startDate }, set: { viewModel.startDate = $0 })
}
@ -366,12 +400,38 @@ struct RealNameAuthView: View {
///
private func submit() async {
do {
try await viewModel.submit(api: profileAPI)
try await viewModel.submit(
api: profileAPI,
uploader: ossUploadService,
scenicId: accountContext.currentScenic?.id ?? 0
)
} catch {
viewModel.statusMessage = error.localizedDescription
toastCenter.show(error.localizedDescription)
}
}
///
private func prepareIdentityImage(from item: PhotosPickerItem?, side: RealNameImageSide) async {
guard let item else { return }
defer {
switch side {
case .front:
pickedFrontItem = nil
case .back:
pickedBackItem = nil
}
}
do {
guard let data = try await item.loadTransferable(type: Data.self) else {
toastCenter.show("请选择有效的证件图片")
return
}
try viewModel.prepareIdentityImage(data: data, side: side)
} catch {
toastCenter.show(error.localizedDescription)
}
}
}
private extension String {