You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

676 lines
24 KiB

import SwiftUI
import CoreData
struct HistoryView: View {
@StateObject private var coreDataManager = CoreDataManager.shared
@State private var searchText = ""
@State private var selectedFilter: HistoryFilter = .all
@State private var showingCreateSheet = false
@State private var itemToDelete: HistoryItem?
@State private var showingDeleteAlert = false
@State private var showingClearConfirmSheet = false
@State private var allHistoryItems: [HistoryItem] = []
@State private var isLoading = false
@State private var refreshTrigger = false
@State private var isBatchDeleteMode = false
@State private var selectedItemsForDelete: Set<UUID> = []
enum HistoryFilter: String, CaseIterable {
case all = "all"
case barcode = "barcode"
case qrcode = "qrcode"
case scanned = "scanned"
case created = "created"
case favorites = "favorites"
var displayName: String {
switch self {
case .all:
return "全部"
case .barcode:
return "条形码"
case .qrcode:
return "二维码"
case .scanned:
return "扫描获得"
case .created:
return "手动创建"
case .favorites:
return "收藏"
}
}
var icon: String {
switch self {
case .all:
return "list.bullet"
case .barcode:
return "barcode"
case .qrcode:
return "qrcode"
case .scanned:
return "camera.viewfinder"
case .created:
return "plus.circle"
case .favorites:
return "heart.fill"
}
}
}
var filteredItems: [HistoryItem] {
// 使 refreshTrigger
let _ = refreshTrigger
let searchResults = allHistoryItems.filter { item in
if !searchText.isEmpty {
let content = item.content ?? ""
let barcodeType = item.barcodeType ?? ""
let qrCodeType = item.qrCodeType ?? ""
return content.localizedCaseInsensitiveContains(searchText) ||
barcodeType.localizedCaseInsensitiveContains(searchText) ||
qrCodeType.localizedCaseInsensitiveContains(searchText)
}
return true
}
switch selectedFilter {
case .all:
return searchResults
case .barcode:
return searchResults.filter { $0.dataType == DataType.barcode.rawValue }
case .qrcode:
return searchResults.filter { $0.dataType == DataType.qrcode.rawValue }
case .scanned:
return searchResults.filter { $0.dataSource == DataSource.scanned.rawValue }
case .created:
return searchResults.filter { $0.dataSource == DataSource.created.rawValue }
case .favorites:
return searchResults.filter { $0.isFavorite }
}
}
var body: some View {
VStack(spacing: 0) {
//
searchBar
//
filterBar
//
if filteredItems.isEmpty {
emptyStateView
} else {
historyList
}
}
.navigationTitle("历史记录")
.navigationBarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
HStack(spacing: 16) {
if isBatchDeleteMode {
//
Button(action: {
// /
if selectedItemsForDelete.count == filteredItems.count {
selectedItemsForDelete.removeAll()
} else {
selectedItemsForDelete = Set(filteredItems.compactMap { $0.id })
}
}) {
Image(systemName: selectedItemsForDelete.count == filteredItems.count ? "checkmark.rectangle.fill" : "rectangle.on.rectangle")
.foregroundColor(.blue)
}
Button(action: {
if !selectedItemsForDelete.isEmpty {
deleteSelectedItems()
}
}) {
Image(systemName: "trash.fill")
.foregroundColor(.red)
}
.disabled(selectedItemsForDelete.isEmpty)
//
Button(action: {
exitBatchDeleteMode()
}) {
Image(systemName: "xmark.circle")
.foregroundColor(.gray)
}
} else {
//
//
if !allHistoryItems.isEmpty {
Button(action: {
enterBatchDeleteMode()
}) {
Image(systemName: "trash")
.foregroundColor(.red)
}
}
Button(action: {
showingCreateSheet = true
}) {
Image(systemName: "plus")
}
}
}
}
}
.sheet(isPresented: $showingCreateSheet) {
CreateCodeView()
}
.sheet(isPresented: $showingClearConfirmSheet) {
ClearHistoryConfirmView(
isPresented: $showingClearConfirmSheet,
onConfirm: clearHistory
)
}
.alert("删除确认", isPresented: $showingDeleteAlert) {
Button("取消", role: .cancel) { }
Button("删除", role: .destructive) {
if let item = itemToDelete {
deleteHistoryItem(item)
itemToDelete = nil
}
}
} message: {
if let item = itemToDelete {
Text("确定要删除这条记录吗?\n内容:\(item.content ?? "")")
}
}
.onAppear {
loadHistoryItems()
}
}
// MARK: -
private func loadHistoryItems() {
isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
allHistoryItems = coreDataManager.fetchHistoryItems()
isLoading = false
}
}
// MARK: -
private func filterAction(for filter: HistoryFilter) {
//
selectedFilter = filter
}
// MARK: -
private func clearHistory() {
coreDataManager.clearAllHistory()
allHistoryItems.removeAll()
refreshTrigger.toggle()
}
// MARK: -
private func toggleFavorite(_ item: HistoryItem) {
// Core Data
item.isFavorite.toggle()
coreDataManager.save()
//
if let index = allHistoryItems.firstIndex(where: { $0.id == item.id }) {
allHistoryItems[index].isFavorite = item.isFavorite
}
//
refreshTrigger.toggle()
}
// MARK: -
private func deleteHistoryItem(_ item: HistoryItem) {
coreDataManager.deleteHistoryItem(item)
//
allHistoryItems.removeAll { $0.id == item.id }
refreshTrigger.toggle()
}
// MARK: -
private func showDeleteConfirmation(for item: HistoryItem) {
itemToDelete = item
showingDeleteAlert = true
}
// MARK: -
private func enterBatchDeleteMode() {
isBatchDeleteMode = true
//
selectedItemsForDelete = Set(filteredItems.compactMap { $0.id })
}
private func exitBatchDeleteMode() {
isBatchDeleteMode = false
selectedItemsForDelete.removeAll()
}
private func deleteSelectedItems() {
//
let itemsToDelete = allHistoryItems.filter { item in
guard let id = item.id else { return false }
return selectedItemsForDelete.contains(id)
}
//
for item in itemsToDelete {
coreDataManager.deleteHistoryItem(item)
}
//
allHistoryItems.removeAll { item in
guard let id = item.id else { return false }
return selectedItemsForDelete.contains(id)
}
// 退
selectedItemsForDelete.removeAll()
isBatchDeleteMode = false
//
DispatchQueue.main.async {
allHistoryItems = coreDataManager.fetchHistoryItems()
refreshTrigger.toggle()
}
}
// MARK: -
private var searchBar: some View {
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.gray)
TextField("搜索历史记录...", text: $searchText)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding(.horizontal)
.padding(.vertical, 8)
.background(Color(.systemBackground))
}
// MARK: -
private var filterBar: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(HistoryFilter.allCases, id: \.self) { filter in
FilterChip(
filter: filter,
isSelected: selectedFilter == filter,
isLoading: false,
action: {
filterAction(for: filter)
}
)
}
}
.padding(.horizontal)
}
.padding(.vertical, 8)
.background(Color(.systemBackground))
}
// MARK: -
private var historyList: some View {
List {
if isLoading {
HStack {
Spacer()
VStack(spacing: 16) {
ProgressView()
.scaleEffect(1.2)
Text("加载中...")
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.vertical, 40)
Spacer()
}
} else {
ForEach(filteredItems) { item in
HistoryItemRow(
item: item,
onToggleFavorite: {
toggleFavorite(item)
},
onDelete: {
showDeleteConfirmation(for: item)
},
isBatchDeleteMode: isBatchDeleteMode,
isSelected: selectedItemsForDelete.contains(item.id ?? UUID()),
onToggleSelection: {
if let id = item.id {
if selectedItemsForDelete.contains(id) {
selectedItemsForDelete.remove(id)
} else {
selectedItemsForDelete.insert(id)
}
}
}
)
}
}
}
.listStyle(PlainListStyle())
}
// MARK: -
private var emptyStateView: some View {
VStack(spacing: 20) {
Image(systemName: "clock.arrow.circlepath")
.font(.system(size: 60))
.foregroundColor(.gray)
Text("暂无历史记录")
.font(.title2)
.fontWeight(.medium)
.foregroundColor(.gray)
Text("扫描二维码或手动创建来开始记录")
.font(.body)
.foregroundColor(.gray)
.multilineTextAlignment(.center)
Button(action: {
showingCreateSheet = true
}) {
HStack {
Image(systemName: "plus.circle.fill")
Text("创建第一个记录")
}
.font(.headline)
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
}
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
#Preview {
NavigationView {
HistoryView()
}
}
// MARK: -
struct FilterChip: View {
let filter: HistoryView.HistoryFilter
let isSelected: Bool
let isLoading: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 6) {
Image(systemName: filter.icon)
.font(.system(size: 14))
Text(filter.displayName)
.font(.system(size: 14, weight: .medium))
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(
RoundedRectangle(cornerRadius: 20)
.fill(isSelected ? Color.blue : Color(.systemGray5))
)
.foregroundColor(isSelected ? .white : .primary)
.scaleEffect(isSelected ? 1.05 : 1.0)
}
.buttonStyle(OptimizedFilterChipButtonStyle(isSelected: isSelected))
.animation(.easeInOut(duration: 0.2), value: isSelected)
}
}
// MARK: -
struct OptimizedFilterChipButtonStyle: ButtonStyle {
let isSelected: Bool
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
.opacity(configuration.isPressed ? 0.9 : 1.0)
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
}
}
// MARK: -
struct FilterChipButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
.opacity(configuration.isPressed ? 0.8 : 1.0)
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
}
}
// MARK: -
struct HistoryItemRow: View {
let item: HistoryItem
let onToggleFavorite: () -> Void
let onDelete: () -> Void
let isBatchDeleteMode: Bool
let isSelected: Bool
let onToggleSelection: () -> Void
var body: some View {
HStack(spacing: 12) {
//
if isBatchDeleteMode {
Button(action: onToggleSelection) {
Image(systemName: isSelected ? "checkmark.square.fill" : "square")
.font(.system(size: 20))
.foregroundColor(isSelected ? .blue : .gray)
}
.buttonStyle(PlainButtonStyle())
}
//
VStack {
if let dataTypeString = item.dataType,
let dataType = DataType(rawValue: dataTypeString) {
Image(systemName: dataType.icon)
.font(.system(size: 24))
.foregroundColor(.blue)
}
if let dataSourceString = item.dataSource,
let dataSource = DataSource(rawValue: dataSourceString) {
Image(systemName: dataSource.icon)
.font(.system(size: 12))
.foregroundColor(.gray)
}
}
.frame(width: 40)
//
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(item.content ?? "")
.font(.headline)
.lineLimit(2)
Spacer()
Button(action: onToggleFavorite) {
Image(systemName: item.isFavorite ? "heart.fill" : "heart")
.foregroundColor(item.isFavorite ? .red : .gray)
}
.buttonStyle(PlainButtonStyle())
}
HStack {
//
if let dataTypeString = item.dataType,
let dataType = DataType(rawValue: dataTypeString) {
HStack(spacing: 4) {
Image(systemName: dataType.icon)
.font(.system(size: 12))
Text(dataType.displayName)
.font(.caption)
}
.padding(.horizontal, 8)
.padding(.vertical, 2)
.background(Color.blue.opacity(0.1))
.foregroundColor(.blue)
.cornerRadius(8)
}
//
if let barcodeTypeString = item.barcodeType,
let barcodeType = BarcodeType(rawValue: barcodeTypeString) {
HStack(spacing: 4) {
Image(systemName: barcodeType.icon)
.font(.system(size: 12))
Text(barcodeType.displayName)
.font(.caption)
}
.padding(.horizontal, 8)
.padding(.vertical, 2)
.background(Color.green.opacity(0.1))
.foregroundColor(.green)
.cornerRadius(8)
}
if let qrCodeTypeString = item.qrCodeType,
let qrCodeType = QRCodeType(rawValue: qrCodeTypeString) {
HStack(spacing: 4) {
Image(systemName: qrCodeType.icon)
.font(.system(size: 12))
Text(qrCodeType.displayName)
.font(.caption)
}
.padding(.horizontal, 8)
.padding(.vertical, 2)
.background(Color.orange.opacity(0.1))
.foregroundColor(.orange)
.cornerRadius(8)
}
Spacer()
//
if let createdAt = item.createdAt {
Text(formatDate(createdAt))
.font(.caption)
.foregroundColor(.gray)
}
}
}
Spacer()
}
.padding(.vertical, 8)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button("删除", role: .destructive) {
onDelete()
}
}
.background(
//
Group {
if item.dataType == DataType.qrcode.rawValue {
NavigationLink(
destination: QRCodeDetailView(historyItem: item),
label: { EmptyView() }
)
} else if item.dataType == DataType.barcode.rawValue {
NavigationLink(
destination: BarcodeDetailView(historyItem: item),
label: { EmptyView() }
)
}
}
)
}
private func formatDate(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .short
return formatter.string(from: date)
}
}
// MARK: -
struct ClearHistoryConfirmView: View {
@Binding var isPresented: Bool
let onConfirm: () -> Void
var body: some View {
NavigationView {
VStack(spacing: 20) {
//
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 50))
.foregroundColor(.red)
//
Text("清空历史记录")
.font(.title2)
.fontWeight(.bold)
//
Text("此操作将删除所有历史记录,且不可撤销")
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
Spacer()
//
VStack(spacing: 12) {
//
Button(action: {
onConfirm()
isPresented = false
}) {
HStack {
Image(systemName: "trash.fill")
Text("确认删除")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.red)
.foregroundColor(.white)
.cornerRadius(10)
}
//
Button(action: {
isPresented = false
}) {
Text("取消")
.frame(maxWidth: .infinity)
.padding()
.background(Color(.systemGray5))
.foregroundColor(.primary)
.cornerRadius(10)
}
}
}
.padding(20)
.navigationTitle("确认删除")
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("关闭") {
isPresented = false
}
}
}
}
}
}