Files
e-party-iOS/yana/Utils/TCCos/Views/COSErrorView.swift
edwinQQQ b966e24532 feat: 更新COSManager和相关视图以增强图片上传功能
- 修改COSManagerAdapter以支持新的TCCos组件,确保与腾讯云COS的兼容性。
- 在CreateFeedFeature中新增图片上传相关状态和Action,优化图片选择与上传逻辑。
- 更新CreateFeedView以整合图片上传功能,提升用户体验。
- 在多个视图中添加键盘状态管理,改善用户交互体验。
- 新增COS相关的测试文件,确保功能的正确性和稳定性。
2025-07-31 11:41:56 +08:00

395 lines
12 KiB
Swift

import SwiftUI
import ComposableArchitecture
// MARK: - COS
/// COS
///
public struct COSErrorView: View {
// MARK: - Properties
let store: StoreOf<COSFeature>
// MARK: - Initialization
public init(store: StoreOf<COSFeature>) {
self.store = store
}
// MARK: - Body
public var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
VStack(spacing: 16) {
//
ErrorSummaryView(viewStore: viewStore)
//
ErrorDetailsView(viewStore: viewStore)
//
RecoveryActionsView(store: store)
Spacer()
}
.padding()
}
}
}
// MARK: -
///
private struct ErrorSummaryView: View {
let viewStore: ViewStore<COSFeature.State, COSFeature.Action>
var body: some View {
let hasErrors = hasAnyErrors
VStack(spacing: 12) {
HStack {
Image(systemName: hasErrors ? "exclamationmark.triangle.fill" : "checkmark.circle.fill")
.foregroundColor(hasErrors ? .red : .green)
.font(.title2)
Text(hasErrors ? "发现问题" : "系统正常")
.font(.headline)
.foregroundColor(hasErrors ? .red : .green)
Spacer()
}
if hasErrors {
Text("检测到以下问题,请查看详情并尝试恢复")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
} else {
Text("所有服务运行正常")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding()
.background(hasErrors ? Color(.systemRed).opacity(0.1) : Color(.systemGreen).opacity(0.1))
.cornerRadius(12)
}
private var hasAnyErrors: Bool {
viewStore.tokenState?.error != nil ||
viewStore.uploadState?.error != nil ||
viewStore.configurationState?.error != nil ||
viewStore.configurationState?.serviceStatus.isFailed == true ||
(viewStore.tokenState?.currentToken?.isExpired == true)
}
}
// MARK: -
///
private struct ErrorDetailsView: View {
let viewStore: ViewStore<COSFeature.State, COSFeature.Action>
var body: some View {
VStack(spacing: 12) {
Text("错误详情")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
LazyVStack(spacing: 8) {
// Token
if let tokenError = viewStore.tokenState?.error {
ErrorDetailCard(
title: "Token 错误",
message: tokenError,
icon: "key.fill",
color: .red,
suggestions: tokenErrorSuggestions
)
}
// Token
if let token = viewStore.tokenState?.currentToken, token.isExpired {
ErrorDetailCard(
title: "Token 已过期",
message: "Token 已于 \(formatRelativeTime(token.expirationDate)) 过期",
icon: "clock.fill",
color: .orange,
suggestions: ["点击\"刷新 Token\"按钮获取新的 Token"]
)
}
//
if let configError = viewStore.configurationState?.error {
ErrorDetailCard(
title: "配置错误",
message: configError,
icon: "gear.fill",
color: .red,
suggestions: ["点击\"重置\"按钮重新初始化服务"]
)
}
//
if case .failed(let error) = viewStore.configurationState?.serviceStatus {
ErrorDetailCard(
title: "服务初始化失败",
message: error,
icon: "server.rack.fill",
color: .red,
suggestions: ["点击\"重置\"按钮重新初始化", "检查网络连接"]
)
}
//
if let uploadError = viewStore.uploadState?.error {
ErrorDetailCard(
title: "上传错误",
message: uploadError,
icon: "arrow.up.circle.fill",
color: .red,
suggestions: ["点击\"重试\"按钮重新上传", "检查网络连接"]
)
}
//
if !hasAnyErrors {
VStack(spacing: 8) {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
.font(.title2)
Text("暂无错误")
.font(.headline)
.foregroundColor(.green)
Text("所有服务运行正常")
.font(.caption)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color(.systemGreen).opacity(0.1))
.cornerRadius(8)
}
}
}
}
private var hasAnyErrors: Bool {
viewStore.tokenState?.error != nil ||
viewStore.uploadState?.error != nil ||
viewStore.configurationState?.error != nil ||
viewStore.configurationState?.serviceStatus.isFailed == true ||
(viewStore.tokenState?.currentToken?.isExpired == true)
}
private var tokenErrorSuggestions: [String] {
["点击\"获取 Token\"按钮重新获取", "检查网络连接", "联系技术支持"]
}
private func formatRelativeTime(_ date: Date) -> String {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .full
return formatter.localizedString(for: date, relativeTo: Date())
}
}
// MARK: -
///
private struct ErrorDetailCard: View {
let title: String
let message: String
let icon: String
let color: Color
let suggestions: [String]
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: icon)
.foregroundColor(color)
Text(title)
.font(.headline)
.foregroundColor(color)
Spacer()
}
Text(message)
.font(.caption)
.foregroundColor(.secondary)
if !suggestions.isEmpty {
VStack(alignment: .leading, spacing: 4) {
Text("建议操作:")
.font(.caption)
.fontWeight(.medium)
.foregroundColor(.secondary)
ForEach(suggestions, id: \.self) { suggestion in
HStack(alignment: .top, spacing: 4) {
Text("")
.font(.caption)
.foregroundColor(.secondary)
Text(suggestion)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
}
}
.padding()
.background(color.opacity(0.1))
.cornerRadius(8)
}
}
// MARK: -
///
private struct RecoveryActionsView: View {
let store: StoreOf<COSFeature>
var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
VStack(spacing: 12) {
Text("恢复操作")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible())
], spacing: 12) {
// Token
RecoveryButton(
title: "获取 Token",
icon: "key.fill",
color: .blue,
isDisabled: viewStore.tokenState?.isLoading == true
) {
viewStore.send(.token(.getToken))
}
// Token
RecoveryButton(
title: "刷新 Token",
icon: "arrow.clockwise",
color: .green,
isDisabled: viewStore.tokenState?.isLoading == true
) {
viewStore.send(.token(.refreshToken))
}
//
RecoveryButton(
title: "重试",
icon: "arrow.clockwise.circle",
color: .orange
) {
viewStore.send(.retry)
}
//
RecoveryButton(
title: "重置",
icon: "trash.fill",
color: .red
) {
viewStore.send(.resetAll)
}
}
//
Button {
viewStore.send(.checkHealth)
} label: {
HStack {
Image(systemName: "heart.fill")
Text("健康检查")
}
.frame(maxWidth: .infinity)
.padding()
}
.buttonStyle(.bordered)
.font(.caption)
}
}
}
}
// MARK: -
///
private struct RecoveryButton: View {
let title: String
let icon: String
let color: Color
let isDisabled: Bool
let action: () -> Void
init(
title: String,
icon: String,
color: Color,
isDisabled: Bool = false,
action: @escaping () -> Void
) {
self.title = title
self.icon = icon
self.color = color
self.isDisabled = isDisabled
self.action = action
}
var body: some View {
Button(action: action) {
VStack(spacing: 8) {
Image(systemName: icon)
.font(.title2)
.foregroundColor(isDisabled ? .gray : color)
Text(title)
.font(.caption)
.fontWeight(.medium)
.foregroundColor(isDisabled ? .gray : color)
}
.frame(maxWidth: .infinity)
.padding()
.background(isDisabled ? Color(.systemGray5) : color.opacity(0.1))
.cornerRadius(8)
}
.disabled(isDisabled)
}
}
// MARK: -
extension COSServiceStatus {
var isFailed: Bool {
switch self {
case .failed:
return true
default:
return false
}
}
}
// MARK: -
#Preview {
COSErrorView(
store: Store(
initialState: COSFeature.State(),
reducer: { COSFeature() }
)
)
}