217 lines
8.3 KiB
Swift
217 lines
8.3 KiB
Swift
//
|
||
// WordsView.swift
|
||
// AIGrammar
|
||
//
|
||
// Created by oscar on 2024/3/27.
|
||
//
|
||
|
||
import SwiftUI
|
||
import ToastUI
|
||
|
||
struct WordsView: View {
|
||
@EnvironmentObject var globalEnv: GlobalEnvironment // 引入环境对象
|
||
|
||
@State private var searchText = ""
|
||
@State private var showingResults = false
|
||
@State private var res = WordDetails()
|
||
@State private var isLoading = false // 控制加载指示器的显示
|
||
@State private var showingToast = false // 控制是否显示toast
|
||
@State private var toastText = ""
|
||
|
||
var body: some View {
|
||
NavigationView {
|
||
ZStack {
|
||
// 设置背景色
|
||
Color.pink.opacity(0.2).edgesIgnoringSafeArea(.all)
|
||
|
||
VStack(spacing: 0) {
|
||
// 搜索框区域
|
||
HStack {
|
||
TextField("word", text: $searchText, onCommit: fetchWordData)
|
||
.padding(.horizontal, 40)
|
||
.padding(.vertical, 10)
|
||
.background(Color(.systemGray6))
|
||
.cornerRadius(8)
|
||
.font(.headline)
|
||
.overlay(
|
||
Image(systemName: "magnifyingglass")
|
||
.foregroundColor(.gray)
|
||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading).padding(.leading, 8)
|
||
)
|
||
Spacer()
|
||
|
||
Button("Cancel") {
|
||
self.searchText = ""
|
||
self.showingResults = false
|
||
self.hideKeyboard()
|
||
}
|
||
}
|
||
.background(Color(.systemPink).opacity(0.2))
|
||
.cornerRadius(5) // 边框圆角
|
||
.shadow(radius: 2) // 轻微阴影
|
||
.padding(8)
|
||
|
||
|
||
// 使用 Spacer 来推动搜索框保持在顶部
|
||
if !showingResults {
|
||
Spacer()
|
||
}
|
||
|
||
if showingResults {
|
||
// 结果显示区域
|
||
HStack {
|
||
List {
|
||
Section(header: Text("Definitions").font(.headline)) {
|
||
ForEach(res.explanations, id: \.self) { definition in
|
||
Text(definition)
|
||
.font(.system(.subheadline))
|
||
.padding(.leading, 4)
|
||
}
|
||
}
|
||
|
||
Section(header: Text("Common Phrases").font(.headline)) {
|
||
ForEach(res.phrases, id: \.self) { phrase in
|
||
Text(phrase)
|
||
.font(.system(.subheadline))
|
||
.padding(.leading, 4)
|
||
}
|
||
}
|
||
|
||
Section(header: Text("Synonyms").font(.system(size: UIFont.systemFontSize * 1.2))) {
|
||
ForEach(res.synonyms, id: \.self) { synonym in
|
||
Text(synonym)
|
||
.font(.system(.subheadline))
|
||
.padding(.leading, 4)
|
||
}
|
||
}
|
||
}
|
||
.padding(8)
|
||
.cornerRadius(5) // 边框圆角
|
||
.frame(maxHeight: .infinity) // 限制ErrorCard占用半个屏幕 */
|
||
}
|
||
.listStyle(GroupedListStyle()) // 使用分组列表样式,以适应背景
|
||
}
|
||
}
|
||
.toast(isPresented: $showingToast, dismissAfter: globalEnvironment.toastPresentMsNormal) {
|
||
HStack {
|
||
Image(systemName: "exclamationmark.bubble")
|
||
.foregroundColor(.yellow)
|
||
Text(toastText)
|
||
.foregroundColor(.black)
|
||
}
|
||
.padding()
|
||
.background(Color.white)
|
||
.cornerRadius(8)
|
||
.shadow(radius: 10)
|
||
}
|
||
|
||
// 加载指示器
|
||
if isLoading {
|
||
LoadingView()
|
||
}
|
||
}
|
||
.onTapGesture {
|
||
// 点击任何非TextField区域时收起键盘
|
||
self.hideKeyboard()
|
||
|
||
}
|
||
.navigationBarTitle("Words", displayMode: .inline)
|
||
}
|
||
|
||
}
|
||
|
||
private func hideKeyboard() {
|
||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||
}
|
||
|
||
// 模拟加载数据
|
||
func loadData() {
|
||
isLoading = true
|
||
}
|
||
func loadComplete(){
|
||
isLoading = false
|
||
}
|
||
|
||
private func fetchWordData() {
|
||
// 清除输入内容前后的空格
|
||
let trimmedText = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
|
||
// 检查是否为空
|
||
if trimmedText.isEmpty {
|
||
showingToast = true
|
||
toastText = "Please enter some text."
|
||
return // 提前返回,不执行网络请求
|
||
}
|
||
|
||
// 检查长度是否超过200个字符
|
||
if trimmedText.count > globalEnvironment.MaxLenWords {
|
||
showingToast = true
|
||
toastText = "Input too long. Please re-enter the text."
|
||
return // 提前返回,不执行网络请求
|
||
}
|
||
|
||
// 检查是否含有非法字符
|
||
let checkRes = CommonFunc.shared.validateInputEng(input: trimmedText, isSingleWord: true)
|
||
if !checkRes.isValid {
|
||
showingToast = true
|
||
toastText = checkRes.message
|
||
return
|
||
}
|
||
/*
|
||
if trimmedText.range(of: "[^a-zA-Z]", options: .regularExpression) != nil {
|
||
showingToast = true
|
||
toastText = "Please enter valid characters."
|
||
return // 提前返回,不执行网络请求
|
||
}
|
||
*/
|
||
|
||
loadData() // 添加提交的提示
|
||
NetworkManager.shared.fetchWordDetails(inputText: trimmedText) { result in
|
||
switch result {
|
||
case .success(let wordDetails):
|
||
DispatchQueue.main.async {
|
||
res = wordDetails
|
||
showingResults = true
|
||
}
|
||
logger.info("fetch words succ.", context: ["word":wordDetails.word, "explanations":wordDetails.explanations, "phrases":wordDetails.phrases, "synonyms":wordDetails.synonyms])
|
||
case .failure(let error):
|
||
switch error {
|
||
case .businessError(let ret, let message):
|
||
// 业务错误,比如无免费可用次数等,需要处理逻辑。
|
||
logger.error("Business error - Ret: \(ret), Message: \(message)")
|
||
DispatchQueue.main.async {
|
||
showingToast = true
|
||
switch ret{
|
||
case globalEnvironment.RetCodeFreeLimited:
|
||
toastText = globalEnvironment.FreeLimitedToast
|
||
case globalEnvironment.RetCodeDirtyInput:
|
||
toastText = globalEnvironment.DirtyInputErrToast
|
||
default:
|
||
toastText = globalEnvironment.OtherServerErrToast
|
||
}
|
||
}
|
||
case .other(let error):
|
||
logger.error("network error occurred: \(error.localizedDescription)")
|
||
DispatchQueue.main.async {
|
||
showingToast = true
|
||
toastText = globalEnvironment.NetWorkErrToast
|
||
}
|
||
}
|
||
}
|
||
loadComplete()
|
||
}
|
||
}
|
||
}
|
||
|
||
struct WordsView_Previews: PreviewProvider {
|
||
static var previews: some View {
|
||
WordsView()
|
||
}
|
||
}
|
||
|
||
|
||
|
||
#Preview {
|
||
WordsView()
|
||
}
|