// // ScenicPromotionSettingsSharedViews.swift // suixinkan // // Created by Codex on 2026/7/2. // import SwiftUI /// 设置页通用卡片容器。 struct ScenicPromotionSettingsCard: View { let title: String let subtitle: String let icon: String let tint: Color @ViewBuilder let content: Content var body: some View { VStack(alignment: .leading, spacing: 14) { HStack(alignment: .top, spacing: 12) { Image(systemName: icon) .font(.system(size: 18, weight: .bold)) .foregroundStyle(tint) .frame(width: 40, height: 40) .background(tint.opacity(0.12), in: RoundedRectangle(cornerRadius: 12)) VStack(alignment: .leading, spacing: 5) { Text(title) .font(.system(size: 18, weight: .heavy)) .foregroundStyle(AppDesign.textPrimary) Text(subtitle) .font(.system(size: 13, weight: .medium)) .foregroundStyle(AppDesign.textSecondary) .fixedSize(horizontal: false, vertical: true) } Spacer(minLength: 0) } content } .padding(16) .frame(maxWidth: .infinity, alignment: .leading) .background(.white, in: RoundedRectangle(cornerRadius: 18)) .overlay( RoundedRectangle(cornerRadius: 18) .stroke(AppDesign.border, lineWidth: 1) ) } } /// 设置页下拉选择框样式。 struct ScenicPromotionSettingsSelectionField: View { let title: String let subtitle: String let icon: String var body: some View { HStack(spacing: 12) { VStack(alignment: .leading, spacing: 5) { Text(title) .font(.system(size: 15, weight: .bold)) .foregroundStyle(AppDesign.textPrimary) .lineLimit(2) .multilineTextAlignment(.leading) .minimumScaleFactor(0.82) Text(subtitle) .font(.system(size: 12, weight: .medium)) .foregroundStyle(AppDesign.textSecondary) } Spacer(minLength: 0) Image(systemName: icon) .font(.system(size: 13, weight: .bold)) .foregroundStyle(AppDesign.primary) .frame(width: 30, height: 30) .background(AppDesign.primarySoft, in: Circle()) } .padding(14) .frame(maxWidth: .infinity, alignment: .leading) .background(AppDesign.primarySoft, in: RoundedRectangle(cornerRadius: 14)) .overlay( RoundedRectangle(cornerRadius: 14) .stroke(Color(hex: 0xCFE1FF), lineWidth: 1) ) } } /// 设置页表单内联提示。 struct ScenicPromotionSettingsInlineNotice: View { let text: String let tint: Color var body: some View { Label(text, systemImage: "exclamationmark.circle.fill") .font(.system(size: 13, weight: .semibold)) .foregroundStyle(tint) .padding(.horizontal, 12) .padding(.vertical, 9) .frame(maxWidth: .infinity, alignment: .leading) .background(tint.opacity(0.1), in: RoundedRectangle(cornerRadius: 12)) } } /// 设置页某个素材点当前没有素材时的空状态。 struct ScenicPromotionSettingsMaterialEmptyView: View { let kind: ScenicPromotionUploadKind var body: some View { VStack(spacing: 8) { Image(systemName: kind.iconName) .font(.system(size: 28, weight: .semibold)) .foregroundStyle(AppDesign.primary) .frame(width: 58, height: 58) .background(AppDesign.primarySoft, in: RoundedRectangle(cornerRadius: 16)) Text(kind.emptyTitle) .font(.system(size: 15, weight: .bold)) .foregroundStyle(AppDesign.textPrimary) Text("点击上传后会把素材保存到当前选择的打卡点") .font(.system(size: 12, weight: .medium)) .foregroundStyle(AppDesign.textSecondary) } .frame(maxWidth: .infinity) .padding(.vertical, 20) .background(Color(hex: 0xF8FAFD), in: RoundedRectangle(cornerRadius: 14)) } } /// 设置页素材行缩略图。 struct ScenicPromotionSettingsMaterialThumbnail: View { let material: ScenicPromotionManagedMaterial var body: some View { ZStack { Image(material.assetName) .resizable() .scaledToFill() .frame(width: 64, height: 48) .clipShape(RoundedRectangle(cornerRadius: 10)) if material.kind == .video { Image(systemName: "play.fill") .font(.system(size: 13, weight: .bold)) .foregroundStyle(.white) .frame(width: 26, height: 26) .background(.black.opacity(0.42), in: Circle()) } } } } /// 设置页 Loading 骨架屏。 struct ScenicPromotionSettingsLoadingView: View { var body: some View { ScrollView(showsIndicators: false) { VStack(spacing: 16) { RoundedRectangle(cornerRadius: 18) .fill(.white) .frame(height: 92) ForEach(0..<4, id: \.self) { _ in RoundedRectangle(cornerRadius: 18) .fill(.white) .frame(height: 132) } } .redacted(reason: .placeholder) .padding(16) } } } /// 设置页 Empty / Error 通用状态视图。 struct ScenicPromotionSettingsFullStateView: View { let icon: String let title: String let message: String let buttonTitle: String let action: () -> Void var body: some View { VStack(spacing: 16) { Spacer() Image(systemName: icon) .font(.system(size: 42, weight: .semibold)) .foregroundStyle(AppDesign.primary) .frame(width: 82, height: 82) .background(AppDesign.primarySoft, in: RoundedRectangle(cornerRadius: 22)) VStack(spacing: 7) { Text(title) .font(.system(size: 20, weight: .bold)) .foregroundStyle(AppDesign.textPrimary) Text(message) .font(.system(size: 14, weight: .medium)) .foregroundStyle(AppDesign.textSecondary) .multilineTextAlignment(.center) } Button(action: action) { Text(buttonTitle) .font(.system(size: 15, weight: .bold)) .foregroundStyle(.white) .frame(width: 132, height: 42) .background(AppDesign.primary, in: Capsule()) } .buttonStyle(.plain) Spacer() } .padding(28) } } #if DEBUG #Preview("设置卡片") { ScenicPromotionSettingsCard( title: "排队打卡点绑定", subtitle: "可多选,用于宣传页展示排队动态", icon: "checklist.checked", tint: AppDesign.primary ) { ScenicPromotionSettingsSelectionField( title: "网红桥排队点、汉地拔冲排队点", subtitle: "2 个已选择", icon: "chevron.down" ) } .padding() .background(AppDesign.background) } #endif