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.

670 lines
23 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 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)
}
}
NavigationLink(destination: CodeTypeSelectionView()) {
Image(systemName: "plus")
}
}
}
}
}
.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)
NavigationLink(destination: CodeTypeSelectionView()) {
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
}
}
}
}
}
}