Files
suixinkan_ios_new/suixinkan/Features/Profile/Views/RealNameAuthView.swift
2026-06-22 15:35:12 +08:00

384 lines
15 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// RealNameAuthView.swift
// suixinkan
//
// Created by Codex on 2026/6/22.
//
import SwiftUI
///
struct RealNameAuthView: View {
@Environment(ProfileAPI.self) private var profileAPI
@Environment(ToastCenter.self) private var toastCenter
@State private var viewModel = RealNameAuthViewModel()
var body: some View {
ScrollView {
VStack(spacing: 14) {
auditStatusPanel
identityCard
privilegeCard
tipsCard
}
.padding(.horizontal, 16)
.padding(.top, 16)
.padding(.bottom, 34)
}
.background(Color(hex: 0xF5F5F5).ignoresSafeArea())
.navigationTitle("实名认证")
.navigationBarTitleDisplayMode(.inline)
.task {
await loadInfo()
}
.overlay {
if viewModel.loading && viewModel.info == nil {
ProgressView()
.controlSize(.large)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white.opacity(0.35))
}
}
}
private var auditStatusPanel: some View {
VStack(spacing: 14) {
auditRow("审核状态", value: viewModel.info?.auditStatusText?.nonEmpty ?? viewModel.auditStatusText(viewModel.info?.auditStatus ?? 0))
auditRow("审核人:", value: viewModel.info?.auditor?.name.nonEmpty ?? "--")
auditRow("审核时间:", value: viewModel.info?.auditAt?.nonEmpty ?? "--")
auditRow("审核备注:", value: viewModel.info?.rejectReason?.nonEmpty ?? "--")
}
.padding(.horizontal, 20)
.padding(.vertical, 22)
.background(Color(hex: 0xEAF4FF), in: RoundedRectangle(cornerRadius: 14))
}
///
private func auditRow(_ title: String, value: String) -> some View {
HStack(alignment: .top) {
Text(title)
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(Color(hex: 0x1687D9))
Spacer(minLength: 20)
Text(value)
.font(.system(size: 16, weight: .medium))
.foregroundStyle(Color(hex: 0x1687D9))
.multilineTextAlignment(.trailing)
}
}
private var identityCard: some View {
VStack(alignment: .leading, spacing: 14) {
identityImageSection(title: "身份证国徽面", url: viewModel.backUrl)
identityImageSection(title: "身份证人像面", url: viewModel.frontUrl)
formField("姓名", text: realNameBinding, placeholder: "请输入姓名")
formField("身份证号码", text: idCardNoBinding, placeholder: "请输入身份证号码")
validitySection
if shouldShowSmsSection {
smsSection
}
urlField("身份证人像面图片 URL", text: frontURLBinding)
urlField("身份证国徽面图片 URL", text: backURLBinding)
submitButton
statusBanner
}
.padding(.horizontal, 18)
.padding(.vertical, 20)
.background(.white, in: RoundedRectangle(cornerRadius: 14))
}
///
private func identityImageSection(title: String, url: String) -> some View {
VStack(alignment: .leading, spacing: 10) {
Text(title)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(Color(hex: 0x222222))
imagePreview(url: url)
}
}
private var validitySection: some View {
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 10) {
Text("有效期")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(Color(hex: 0x222222))
Spacer()
Button {
viewModel.isLongValid.toggle()
} label: {
HStack(spacing: 6) {
Image(systemName: viewModel.isLongValid ? "checkmark.circle.fill" : "circle")
.font(.system(size: 18, weight: .semibold))
Text("长期有效")
.font(.system(size: 15))
}
.foregroundStyle(viewModel.isLongValid ? AppDesign.primary : Color(hex: 0x8EA0AF))
}
.buttonStyle(.plain)
}
HStack(spacing: 12) {
dateField(selection: startDateBinding)
dateField(selection: endDateBinding)
.disabled(viewModel.isLongValid)
.opacity(viewModel.isLongValid ? 0.45 : 1)
}
}
}
private var smsSection: some View {
HStack(spacing: 10) {
TextField("", text: smsCodeBinding, prompt: Text("请输入短信验证码").foregroundColor(AppDesign.placeholder))
.keyboardType(.numberPad)
.font(.system(size: 16))
.appInputFieldStyle(cornerRadius: 8, minHeight: 48)
Button {
Task { await sendCode() }
} label: {
Text(viewModel.sendingCode ? "发送中" : "获取验证码")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(viewModel.sendingCode ? Color(hex: 0x9CA3AF) : .white)
.frame(width: 104, height: 48)
.background(viewModel.sendingCode ? Color(hex: 0xEEF2F7) : AppDesign.primary, in: RoundedRectangle(cornerRadius: 8))
}
.buttonStyle(.plain)
.disabled(viewModel.sendingCode)
}
}
private var submitButton: some View {
Button {
Task { await submit() }
} label: {
Text(viewModel.submitting ? "提交中..." : "下一步")
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.frame(height: 52)
.background(canSubmit ? AppDesign.primary : Color(hex: 0xC9CED6), in: RoundedRectangle(cornerRadius: 10))
}
.buttonStyle(.plain)
.disabled(!canSubmit || viewModel.submitting)
}
@ViewBuilder
private var statusBanner: some View {
if let statusMessage = viewModel.statusMessage {
HStack(spacing: 6) {
Image(systemName: statusMessageIsSuccess ? "checkmark.circle.fill" : "exclamationmark.triangle.fill")
.font(.system(size: 12))
Text(statusMessage)
.font(.system(size: 13))
}
.foregroundStyle(statusMessageIsSuccess ? AppDesign.success : AppDesign.warning)
.padding(.horizontal, 10)
.frame(minHeight: 34, alignment: .leading)
.background((statusMessageIsSuccess ? AppDesign.success : AppDesign.warning).opacity(0.12), in: RoundedRectangle(cornerRadius: 8))
}
}
private var privilegeCard: some View {
VStack(alignment: .leading, spacing: 22) {
Text("实名认证特权")
.font(.system(size: 20, weight: .semibold))
.foregroundStyle(Color(hex: 0x222222))
privilegeRow(icon: "checkmark.circle.fill", title: "开启接单权限", message: "完成实名认证后可立即接单,实名状态对游客可见")
privilegeRow(icon: "person.3.fill", title: "加入营销群", message: "专业摄影师交流群,获取更多流量与技术支持")
privilegeRow(icon: "map.fill", title: "景区选择权限", message: "可自主选择运营景区,获得独家运营资源")
}
.padding(.horizontal, 20)
.padding(.vertical, 22)
.background(.white, in: RoundedRectangle(cornerRadius: 14))
}
///
private func privilegeRow(icon: String, title: String, message: String) -> some View {
HStack(alignment: .top, spacing: 14) {
Image(systemName: icon)
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(.white)
.frame(width: 30, height: 30)
.background(AppDesign.primary, in: Circle())
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(Color(hex: 0x333333))
Text(message)
.font(.system(size: 14))
.foregroundStyle(Color(hex: 0x777777))
.fixedSize(horizontal: false, vertical: true)
}
}
}
private var tipsCard: some View {
VStack(alignment: .leading, spacing: 14) {
Text("温馨提示:")
.font(.system(size: 20, weight: .semibold))
.foregroundStyle(Color(hex: 0x333333))
tipText("请确保填写的信息真实有效")
tipText("身份信息仅用于实名认证,我们将严格保护您的隐私")
tipText("如有疑问请联系客服")
}
.padding(.horizontal, 20)
.padding(.vertical, 22)
.background(.white, in: RoundedRectangle(cornerRadius: 14))
}
///
private func tipText(_ text: String) -> some View {
HStack(alignment: .top, spacing: 8) {
Text("-")
Text(text)
.fixedSize(horizontal: false, vertical: true)
}
.font(.system(size: 15))
.foregroundStyle(Color(hex: 0x666666))
}
///
private func imagePreview(url: String) -> 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
}
}
} else {
placeholderImageContent
}
}
.frame(maxWidth: .infinity)
.frame(height: 166)
.background(Color(hex: 0xFAFAFA))
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color(hex: 0xE0E0E0), style: StrokeStyle(lineWidth: 1, dash: [5, 4]))
)
}
private var placeholderImageContent: some View {
ZStack {
Color(hex: 0xF3F6FA)
Image(systemName: "photo")
.font(.system(size: 26, weight: .semibold))
.foregroundStyle(Color(hex: 0x8A94A6))
}
}
///
private func formField(_ title: String, text: Binding<String>, placeholder: String) -> some View {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(Color(hex: 0x222222))
TextField("", text: text, prompt: Text(placeholder).foregroundColor(AppDesign.placeholder))
.font(.system(size: 16))
.foregroundStyle(Color(hex: 0x333333))
.appInputFieldStyle(cornerRadius: 8, minHeight: 54)
}
}
/// 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)
.labelsHidden()
.datePickerStyle(.compact)
.font(.system(size: 16))
.frame(maxWidth: .infinity)
.frame(height: 54)
.background(Color(hex: 0xF1F1F1), in: RoundedRectangle(cornerRadius: 6))
}
private var canSubmit: Bool {
viewModel.validationMessage == nil && !viewModel.submitting
}
private var shouldShowSmsSection: Bool {
!viewModel.smsCode.isEmpty || viewModel.info?.verified != true
}
private var statusMessageIsSuccess: Bool {
guard let statusMessage = viewModel.statusMessage else { return false }
return statusMessage.contains("成功") || statusMessage.contains("已发送") || statusMessage.contains("已提交")
}
private var realNameBinding: Binding<String> {
Binding(get: { viewModel.realName }, set: { viewModel.realName = $0 })
}
private var idCardNoBinding: Binding<String> {
Binding(get: { viewModel.idCardNo }, set: { viewModel.idCardNo = $0 })
}
private var smsCodeBinding: Binding<String> {
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 })
}
private var endDateBinding: Binding<Date> {
Binding(get: { viewModel.endDate }, set: { viewModel.endDate = $0 })
}
///
private func loadInfo() async {
do {
try await viewModel.load(api: profileAPI)
} catch is CancellationError {
return
} catch {
toastCenter.show(error.localizedDescription)
}
}
///
private func sendCode() async {
do {
try await viewModel.sendCode(api: profileAPI)
} catch {
toastCenter.show(error.localizedDescription)
}
}
///
private func submit() async {
do {
try await viewModel.submit(api: profileAPI)
} catch {
viewModel.statusMessage = error.localizedDescription
toastCenter.show(error.localizedDescription)
}
}
}
private extension String {
///
var nonEmpty: String? {
let text = trimmingCharacters(in: .whitespacesAndNewlines)
return text.isEmpty ? nil : text
}
}