Add OSS upload and Kingfisher image support
This commit is contained in:
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user