
- 修改COSManagerAdapter以支持新的TCCos组件,确保与腾讯云COS的兼容性。 - 在CreateFeedFeature中新增图片上传相关状态和Action,优化图片选择与上传逻辑。 - 更新CreateFeedView以整合图片上传功能,提升用户体验。 - 在多个视图中添加键盘状态管理,改善用户交互体验。 - 新增COS相关的测试文件,确保功能的正确性和稳定性。
395 lines
12 KiB
Swift
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() }
|
|
)
|
|
)
|
|
}
|