import SwiftUI // MARK: - 通用文本编辑器组件 struct TextEditorView: View { let title: String let isRequired: Bool let placeholder: String let text: Binding let maxCharacters: Int let minHeight: CGFloat let icon: String? let isFocused: Bool let onFocusChange: (Bool) -> Void init( title: String, isRequired: Bool = false, placeholder: String, text: Binding, maxCharacters: Int = 1000, minHeight: CGFloat = 120, icon: String? = nil, isFocused: Bool = false, onFocusChange: @escaping (Bool) -> Void = { _ in } ) { self.title = title self.isRequired = isRequired self.placeholder = placeholder self.text = text self.maxCharacters = maxCharacters self.minHeight = minHeight self.icon = icon self.isFocused = isFocused self.onFocusChange = onFocusChange } var body: some View { VStack(alignment: .leading, spacing: 8) { InputTitleView.required(title, icon: icon) ZStack { // 文本编辑器主体 TextEditor(text: text) .frame(minHeight: minHeight) .padding(8) .background(Color(.systemBackground)) .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(isFocused ? Color.blue : Color(.systemGray4), lineWidth: 1) ) .onChange(of: text.wrappedValue) { newValue in // 限制最大字符数 if newValue.count > maxCharacters { text.wrappedValue = String(newValue.prefix(maxCharacters)) } } .onTapGesture { onFocusChange(true) } // 占位符文本 - 左上角对齐 if text.wrappedValue.isEmpty && !isFocused { VStack { HStack { Text(placeholder) .foregroundColor(.secondary) .font(.body) Spacer() } Spacer() } .padding(16) .allowsHitTesting(false) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } } // 字符计数 HStack { Spacer() Text("\(text.wrappedValue.count)/\(maxCharacters)") .font(.caption) .foregroundColor(getCharacterCountColor()) } } } private func getCharacterCountColor() -> Color { let count = text.wrappedValue.count if count >= maxCharacters { return .orange } else if count >= Int(Double(maxCharacters) * 0.9) { return .blue } else { return .secondary } } } // MARK: - 预定义的文本编辑器样式 extension TextEditorView { static func description( title: String, isRequired: Bool = false, placeholder: String, text: Binding, maxCharacters: Int = 500, icon: String? = nil, isFocused: Bool = false, onFocusChange: @escaping (Bool) -> Void = { _ in } ) -> TextEditorView { TextEditorView( title: title, isRequired: isRequired, placeholder: placeholder, text: text, maxCharacters: maxCharacters, minHeight: 100, icon: icon, isFocused: isFocused, onFocusChange: onFocusChange ) } static func longText( title: String, isRequired: Bool = false, placeholder: String, text: Binding, maxCharacters: Int = 1000, icon: String? = nil, isFocused: Bool = false, onFocusChange: @escaping (Bool) -> Void = { _ in } ) -> TextEditorView { TextEditorView( title: title, isRequired: isRequired, placeholder: placeholder, text: text, maxCharacters: maxCharacters, minHeight: 150, icon: icon, isFocused: isFocused, onFocusChange: onFocusChange ) } static func emailBody( title: String, isRequired: Bool = false, placeholder: String, text: Binding, maxCharacters: Int = 1200, icon: String? = nil, isFocused: Bool = false, onFocusChange: @escaping (Bool) -> Void = { _ in } ) -> TextEditorView { TextEditorView( title: title, isRequired: isRequired, placeholder: placeholder, text: text, maxCharacters: maxCharacters, minHeight: 120, icon: icon, isFocused: isFocused, onFocusChange: onFocusChange ) } } #Preview { VStack(spacing: 16) { TextEditorView.description( title: "描述", isRequired: true, placeholder: "请输入描述内容...", text: .constant(""), icon: "text.quote" ) TextEditorView.longText( title: "长文本", isRequired: false, placeholder: "请输入长文本内容...", text: .constant(""), icon: "doc.text" ) TextEditorView.emailBody( title: "邮件正文", isRequired: true, placeholder: "输入邮件正文内容...", text: .constant(""), icon: "envelope" ) } .padding() }