|
|
import SwiftUI
|
|
|
import CoreData
|
|
|
import Combine
|
|
|
|
|
|
struct HistoryView: View {
|
|
|
@EnvironmentObject var coreDataManager: CoreDataManager
|
|
|
@EnvironmentObject var languageManager: LanguageManager
|
|
|
@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 currentPage = 0
|
|
|
@State private var isLoadingMore = false
|
|
|
@State private var hasMoreData = true
|
|
|
@State private var isLoading = false
|
|
|
@State private var refreshTrigger = false
|
|
|
@State private var isBatchDeleteMode = false
|
|
|
@State private var selectedItemsForDelete: Set<UUID> = []
|
|
|
@State private var itemToEdit: HistoryItem?
|
|
|
@State private var showingEditView = false
|
|
|
|
|
|
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 "all".localized
|
|
|
case .barcode:
|
|
|
return "barcode".localized
|
|
|
case .qrcode:
|
|
|
return "qrcode".localized
|
|
|
case .scanned:
|
|
|
return "scanned".localized
|
|
|
case .created:
|
|
|
return "created".localized
|
|
|
case .favorites:
|
|
|
return "favorites".localized
|
|
|
}
|
|
|
}
|
|
|
|
|
|
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 强制触å<EFBFBD>‘计算属性更新
|
|
|
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) {
|
|
|
// æ<EFBFBD>œç´¢æ <EFBFBD>
|
|
|
searchBar
|
|
|
|
|
|
// 过滤器
|
|
|
filterBar
|
|
|
|
|
|
// 内容列表
|
|
|
if filteredItems.isEmpty {
|
|
|
emptyStateView
|
|
|
} else {
|
|
|
historyList
|
|
|
}
|
|
|
}
|
|
|
.navigationTitle("history_records".localized)
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
.navigationBarTitleDisplayMode(.large)
|
|
|
.toolbar {
|
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
|
HStack(spacing: 16) {
|
|
|
if isBatchDeleteMode {
|
|
|
// 批é‡<EFBFBD>åˆ é™¤æ¨¡å¼<EFBFBD>下的按钮
|
|
|
Button(action: {
|
|
|
// 全选/å<EFBFBD>–消全选
|
|
|
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)
|
|
|
|
|
|
// 返回æ£å¸¸çжæ€<EFBFBD>按钮
|
|
|
Button(action: {
|
|
|
exitBatchDeleteMode()
|
|
|
}) {
|
|
|
Image(systemName: "xmark.circle")
|
|
|
.foregroundColor(.gray)
|
|
|
}
|
|
|
} else {
|
|
|
// æ£å¸¸æ¨¡å¼<EFBFBD>下的按钮
|
|
|
// å<EFBFBD>ªæœ‰å½“有记录时æ‰<EFBFBD>æ˜¾ç¤ºåˆ é™¤æŒ‰é’®
|
|
|
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("delete_confirmation".localized, isPresented: $showingDeleteAlert) {
|
|
|
Button("cancel".localized, role: .cancel) { }
|
|
|
Button("delete".localized, role: .destructive) {
|
|
|
if let item = itemToDelete {
|
|
|
deleteHistoryItem(item)
|
|
|
itemToDelete = nil
|
|
|
}
|
|
|
}
|
|
|
} message: {
|
|
|
if let item = itemToDelete {
|
|
|
Text(String(format: "confirm_delete_record".localized, item.content ?? ""))
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
}
|
|
|
}
|
|
|
.onAppear {
|
|
|
loadHistoryItems()
|
|
|
}
|
|
|
.onReceive(coreDataManager.objectWillChange) { _ in
|
|
|
// 当Core Dataæ•°æ<EFBFBD>®å<EFBFBD>‘生å<EFBFBD>˜åŒ–时,é‡<EFBFBD>æ–°åŠ è½½åŽ†å<EFBFBD>²è®°å½•
|
|
|
loadHistoryItems()
|
|
|
}
|
|
|
.background(editNavigationLink)
|
|
|
}
|
|
|
|
|
|
// MARK: - åŠ è½½åŽ†å<EFBFBD>²è®°å½•
|
|
|
private func loadHistoryItems() {
|
|
|
isLoading = true
|
|
|
currentPage = 0
|
|
|
hasMoreData = true
|
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
|
let items = coreDataManager.fetchHistoryItems(page: currentPage)
|
|
|
allHistoryItems = items
|
|
|
|
|
|
// 检查是å<EFBFBD>¦è¿˜æœ‰æ›´å¤šæ•°æ<EFBFBD>®
|
|
|
let nextPageItems = coreDataManager.fetchHistoryItems(page: currentPage + 1)
|
|
|
hasMoreData = !nextPageItems.isEmpty
|
|
|
|
|
|
isLoading = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private func loadMoreHistoryItems() {
|
|
|
guard !isLoadingMore && hasMoreData else { return }
|
|
|
|
|
|
isLoadingMore = true
|
|
|
currentPage += 1
|
|
|
|
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
|
let newItems = coreDataManager.fetchHistoryItems(page: currentPage)
|
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
if newItems.isEmpty {
|
|
|
self.hasMoreData = false
|
|
|
} else {
|
|
|
self.allHistoryItems.append(contentsOf: newItems)
|
|
|
|
|
|
// 检查是å<EFBFBD>¦è¿˜æœ‰æ›´å¤šæ•°æ<EFBFBD>®
|
|
|
let nextPageItems = coreDataManager.fetchHistoryItems(page: self.currentPage + 1)
|
|
|
self.hasMoreData = !nextPageItems.isEmpty
|
|
|
}
|
|
|
self.isLoadingMore = false
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - 过滤器æ“<EFBFBD>作
|
|
|
private func filterAction(for filter: HistoryFilter) {
|
|
|
// 直接切æ<EFBFBD>¢è¿‡æ»¤å™¨ï¼Œæ— 任何延迟
|
|
|
selectedFilter = filter
|
|
|
}
|
|
|
|
|
|
// MARK: - 清空历å<EFBFBD>²è®°å½•
|
|
|
private func clearHistory() {
|
|
|
coreDataManager.clearAllHistory()
|
|
|
allHistoryItems.removeAll()
|
|
|
refreshTrigger.toggle()
|
|
|
}
|
|
|
|
|
|
// MARK: - 切æ<EFBFBD>¢æ”¶è—<EFBFBD>状æ€<EFBFBD>
|
|
|
private func toggleFavorite(_ item: HistoryItem) {
|
|
|
// å…ˆä¿<EFBFBD>å˜åˆ° Core Data
|
|
|
item.isFavorite.toggle()
|
|
|
coreDataManager.save()
|
|
|
|
|
|
// 更新本地缓å˜ï¼Œé<EFBFBD>¿å…<EFBFBD>é‡<EFBFBD>æ–°åŠ è½½æ•°æ<EFBFBD>®
|
|
|
if let index = allHistoryItems.firstIndex(where: { $0.id == item.id }) {
|
|
|
allHistoryItems[index].isFavorite = item.isFavorite
|
|
|
}
|
|
|
|
|
|
// 强制触å<EFBFBD>‘视图刷新
|
|
|
refreshTrigger.toggle()
|
|
|
}
|
|
|
|
|
|
// MARK: - åˆ é™¤åŽ†å<EFBFBD>²è®°å½•
|
|
|
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: - 显示编辑界é<EFBFBD>¢
|
|
|
private func showEditView(for item: HistoryItem) {
|
|
|
itemToEdit = item
|
|
|
showingEditView = true
|
|
|
}
|
|
|
|
|
|
// MARK: - 获å<EFBFBD>–二维ç <EFBFBD>类型
|
|
|
private func getQRCodeType(from item: HistoryItem) -> QRCodeType {
|
|
|
if let qrCodeTypeString = item.qrCodeType,
|
|
|
let qrCodeType = QRCodeType(rawValue: qrCodeTypeString) {
|
|
|
return qrCodeType
|
|
|
}
|
|
|
return .text // 默认类型
|
|
|
}
|
|
|
|
|
|
// MARK: - 获å<EFBFBD>–æ ·å¼<EFBFBD>æ•°æ<EFBFBD>®
|
|
|
private func getStyleData(from item: HistoryItem) -> QRCodeStyleData? {
|
|
|
// 从历å<EFBFBD>²è®°å½•项䏿<EFBFBD><EFBFBD>å<EFBFBD>–æ ·å¼<EFBFBD>æ•°æ<EFBFBD>®
|
|
|
guard let jsonString = item.qrCodeStyleData,
|
|
|
let jsonData = jsonString.data(using: .utf8) else {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
do {
|
|
|
let styleData = try JSONDecoder().decode(QRCodeStyleData.self, from: jsonData)
|
|
|
return styleData
|
|
|
} catch {
|
|
|
print("â<EFBFBD>Œ æ ·å¼<C3A5>æ•°æ<C2B0>®JSONè§£ç <C3A7>失败:\(error)")
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - 编辑导航链接
|
|
|
private var editNavigationLink: some View {
|
|
|
NavigationLink(
|
|
|
destination: Group {
|
|
|
if let itemToEdit = itemToEdit {
|
|
|
QRCodeStyleView(
|
|
|
qrCodeContent: itemToEdit.content ?? "",
|
|
|
qrCodeType: getQRCodeType(from: itemToEdit),
|
|
|
existingStyleData: getStyleData(from: itemToEdit),
|
|
|
historyItemId: itemToEdit.id?.uuidString
|
|
|
)
|
|
|
}
|
|
|
},
|
|
|
isActive: $showingEditView
|
|
|
) {
|
|
|
EmptyView()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - 批é‡<EFBFBD>åˆ é™¤ç›¸å…³æ–¹æ³•
|
|
|
private func enterBatchDeleteMode() {
|
|
|
isBatchDeleteMode = true
|
|
|
// 默认全选当å‰<EFBFBD>过滤å<EFBFBD>Žçš„项目
|
|
|
selectedItemsForDelete = Set(filteredItems.compactMap { $0.id })
|
|
|
}
|
|
|
|
|
|
private func exitBatchDeleteMode() {
|
|
|
isBatchDeleteMode = false
|
|
|
selectedItemsForDelete.removeAll()
|
|
|
}
|
|
|
|
|
|
private func deleteSelectedItems() {
|
|
|
// 获å<EFBFBD>–选ä¸çš„项目
|
|
|
let itemsToDelete = allHistoryItems.filter { item in
|
|
|
guard let id = item.id else { return false }
|
|
|
return selectedItemsForDelete.contains(id)
|
|
|
}
|
|
|
|
|
|
// 批é‡<EFBFBD>åˆ é™¤
|
|
|
for item in itemsToDelete {
|
|
|
coreDataManager.deleteHistoryItem(item)
|
|
|
}
|
|
|
|
|
|
// 从本地缓å˜ä¸ç§»é™¤
|
|
|
allHistoryItems.removeAll { item in
|
|
|
guard let id = item.id else { return false }
|
|
|
return selectedItemsForDelete.contains(id)
|
|
|
}
|
|
|
|
|
|
// 清空选择状æ€<EFBFBD>并退出批é‡<EFBFBD>åˆ é™¤æ¨¡å¼<EFBFBD>
|
|
|
selectedItemsForDelete.removeAll()
|
|
|
isBatchDeleteMode = false
|
|
|
|
|
|
// 强制刷新数æ<EFBFBD>®
|
|
|
DispatchQueue.main.async {
|
|
|
allHistoryItems = coreDataManager.fetchHistoryItems()
|
|
|
refreshTrigger.toggle()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - æ<EFBFBD>œç´¢æ <EFBFBD>
|
|
|
private var searchBar: some View {
|
|
|
HStack {
|
|
|
Image(systemName: "magnifyingglass")
|
|
|
.foregroundColor(.gray)
|
|
|
|
|
|
TextField("search_history_records".localized, text: $searchText)
|
|
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
|
|
}
|
|
|
.padding(.horizontal)
|
|
|
.padding(.vertical, 8)
|
|
|
.background(Color(.systemBackground))
|
|
|
}
|
|
|
|
|
|
// MARK: - 过滤器æ <EFBFBD>
|
|
|
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: - 历å<EFBFBD>²è®°å½•列表
|
|
|
private var historyList: some View {
|
|
|
List {
|
|
|
if isLoading {
|
|
|
HStack {
|
|
|
Spacer()
|
|
|
VStack(spacing: 16) {
|
|
|
ProgressView()
|
|
|
.scaleEffect(1.2)
|
|
|
Text("loading".localized)
|
|
|
.font(.caption)
|
|
|
.foregroundColor(.secondary)
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
}
|
|
|
.padding(.vertical, 40)
|
|
|
Spacer()
|
|
|
}
|
|
|
} else {
|
|
|
ForEach(filteredItems) { item in
|
|
|
HistoryItemRow(
|
|
|
item: item,
|
|
|
onToggleFavorite: {
|
|
|
toggleFavorite(item)
|
|
|
},
|
|
|
onDelete: {
|
|
|
showDeleteConfirmation(for: item)
|
|
|
},
|
|
|
onEdit: {
|
|
|
showEditView(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)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
)
|
|
|
}
|
|
|
|
|
|
// åŠ è½½æ›´å¤šæŒ‰é’®
|
|
|
if hasMoreData && !isLoadingMore {
|
|
|
Button(action: loadMoreHistoryItems) {
|
|
|
HStack {
|
|
|
Image(systemName: "arrow.down.circle")
|
|
|
Text("load_more".localized)
|
|
|
}
|
|
|
.foregroundColor(.blue)
|
|
|
.padding()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// åŠ è½½æ›´å¤šæŒ‡ç¤ºå™¨
|
|
|
if isLoadingMore {
|
|
|
HStack {
|
|
|
Spacer()
|
|
|
ProgressView()
|
|
|
.scaleEffect(0.8)
|
|
|
Text("loading_more".localized)
|
|
|
.font(.caption)
|
|
|
.foregroundColor(.secondary)
|
|
|
Spacer()
|
|
|
}
|
|
|
.padding()
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
.listStyle(PlainListStyle())
|
|
|
}
|
|
|
|
|
|
// MARK: - 空状æ€<EFBFBD>视图
|
|
|
private var emptyStateView: some View {
|
|
|
VStack(spacing: 20) {
|
|
|
Image(systemName: "clock.arrow.circlepath")
|
|
|
.font(.system(size: 60))
|
|
|
.foregroundColor(.gray)
|
|
|
|
|
|
Text("no_history_records".localized)
|
|
|
.font(.title2)
|
|
|
.fontWeight(.medium)
|
|
|
.foregroundColor(.gray)
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
|
|
|
Text("scan_or_create_to_start".localized)
|
|
|
.font(.body)
|
|
|
.foregroundColor(.gray)
|
|
|
.multilineTextAlignment(.center)
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
|
|
|
NavigationLink(destination: CodeTypeSelectionView()) {
|
|
|
HStack {
|
|
|
Image(systemName: "plus.circle.fill")
|
|
|
Text("create_first_record".localized)
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
}
|
|
|
.font(.headline)
|
|
|
.foregroundColor(.white)
|
|
|
.padding()
|
|
|
.background(Color.blue)
|
|
|
.cornerRadius(10)
|
|
|
}
|
|
|
}
|
|
|
.padding()
|
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
#Preview {
|
|
|
NavigationView {
|
|
|
HistoryView()
|
|
|
.environmentObject(LanguageManager.shared)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 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: - ä¼˜åŒ–çš„è¿‡æ»¤å™¨èŠ¯ç‰‡æŒ‰é’®æ ·å¼<EFBFBD>
|
|
|
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: - è¿‡æ»¤å™¨èŠ¯ç‰‡æŒ‰é’®æ ·å¼<EFBFBD>(ä¿<EFBFBD>留以防å<EFBFBD>‘å<EFBFBD>Žå…¼å®¹ï¼‰
|
|
|
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: - 历å<EFBFBD>²è®°å½•项行
|
|
|
struct HistoryItemRow: View {
|
|
|
let item: HistoryItem
|
|
|
let onToggleFavorite: () -> Void
|
|
|
let onDelete: () -> Void
|
|
|
let onEdit: (() -> Void)?
|
|
|
let isBatchDeleteMode: Bool
|
|
|
let isSelected: Bool
|
|
|
let onToggleSelection: () -> Void
|
|
|
|
|
|
var body: some View {
|
|
|
HStack(spacing: 12) {
|
|
|
// 批é‡<EFBFBD>åˆ é™¤æ¨¡å¼<EFBFBD>下的选择框
|
|
|
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)
|
|
|
|
|
|
// 内容信æ<EFBFBD>¯
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
|
HStack {
|
|
|
Text(item.content ?? "")
|
|
|
.font(.headline)
|
|
|
.lineLimit(1)
|
|
|
|
|
|
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(action: {
|
|
|
shareItem(item)
|
|
|
}) {
|
|
|
Image(systemName: "square.and.arrow.up")
|
|
|
}
|
|
|
.tint(.gray)
|
|
|
|
|
|
// 为创建类型的二维ç <EFBFBD>æ<EFBFBD>¡ç›®æ·»åŠ ç¼–è¾‘æŒ‰é’®
|
|
|
if item.dataType == DataType.qrcode.rawValue &&
|
|
|
item.dataSource == DataSource.created.rawValue,
|
|
|
let onEdit = onEdit {
|
|
|
Button(action: onEdit) {
|
|
|
Image(systemName: "pencil")
|
|
|
}
|
|
|
.tint(.blue)
|
|
|
}
|
|
|
|
|
|
Button(action: onDelete) {
|
|
|
Image(systemName: "trash")
|
|
|
}
|
|
|
.tint(.red)
|
|
|
}
|
|
|
.background(
|
|
|
// æ ¹æ<EFBFBD>®æ•°æ<EFBFBD>®ç±»åž‹å’Œæ•°æ<EFBFBD>®æº<EFBFBD>æ·»åŠ å¯¼èˆªé“¾æŽ¥
|
|
|
Group {
|
|
|
if item.dataType == DataType.qrcode.rawValue {
|
|
|
if item.dataSource == DataSource.created.rawValue {
|
|
|
// 创建类型的二维ç <EFBFBD>æ<EFBFBD>¡ç›®ï¼Œå¯¼èˆªåˆ°ä¿<EFBFBD>å˜ç•Œé<EFBFBD>¢
|
|
|
NavigationLink(
|
|
|
destination: QRCodeSavedView(
|
|
|
qrCodeImage: generateQRCodeImage(from: item),
|
|
|
qrCodeContent: item.content ?? "",
|
|
|
qrCodeType: getQRCodeType(from: item),
|
|
|
styleData: getStyleData(from: item),
|
|
|
historyItem: item
|
|
|
),
|
|
|
label: { EmptyView() }
|
|
|
)
|
|
|
} else {
|
|
|
// 扫æ<EFBFBD><EFBFBD>类型的二维ç <EFBFBD>æ<EFBFBD>¡ç›®ï¼Œå¯¼èˆªåˆ°è¯¦æƒ…界é<EFBFBD>¢
|
|
|
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: - QRCodeSavedView 辅助方法
|
|
|
private func generateQRCodeImage(from item: HistoryItem) -> UIImage {
|
|
|
// 从历å<EFBFBD>²è®°å½•项生æˆ<EFBFBD>二维ç <EFBFBD>图片
|
|
|
let content = item.content ?? ""
|
|
|
|
|
|
// 获å<EFBFBD>–æ ·å¼<EFBFBD>æ•°æ<EFBFBD>®
|
|
|
let styleData = getStyleData(from: item)
|
|
|
|
|
|
// 使用高质é‡<EFBFBD>二维ç <EFBFBD>生æˆ<EFBFBD>器
|
|
|
return QRCodeGenerator.generateHighQualityQRCode(
|
|
|
content: content,
|
|
|
styleData: styleData
|
|
|
)
|
|
|
}
|
|
|
|
|
|
// MARK: - 分享功能
|
|
|
private func shareItem(_ item: HistoryItem) {
|
|
|
let content = item.content ?? ""
|
|
|
let activityVC = UIActivityViewController(
|
|
|
activityItems: [content],
|
|
|
applicationActivities: nil
|
|
|
)
|
|
|
|
|
|
// 在iPad上需è¦<EFBFBD>设置popoverPresentationController
|
|
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
|
|
let window = windowScene.windows.first {
|
|
|
if let popover = activityVC.popoverPresentationController {
|
|
|
popover.sourceView = window
|
|
|
popover.sourceRect = CGRect(x: window.bounds.midX, y: window.bounds.midY, width: 0, height: 0)
|
|
|
popover.permittedArrowDirections = []
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 记录Facebook事件 - 分享功能
|
|
|
let contentType = item.dataType == DataType.qrcode.rawValue ? "qr_code" : "barcode"
|
|
|
FacebookEventManager.shared.logShare(contentType: contentType)
|
|
|
|
|
|
// 获å<EFBFBD>–当å‰<EFBFBD>视图控制器并显示分享界é<EFBFBD>¢
|
|
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
|
|
let window = windowScene.windows.first,
|
|
|
let rootViewController = window.rootViewController {
|
|
|
rootViewController.present(activityVC, animated: true)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private func getQRCodeType(from item: HistoryItem) -> QRCodeType {
|
|
|
if let qrCodeTypeString = item.qrCodeType,
|
|
|
let qrCodeType = QRCodeType(rawValue: qrCodeTypeString) {
|
|
|
return qrCodeType
|
|
|
}
|
|
|
return .text // 默认类型
|
|
|
}
|
|
|
|
|
|
private func getStyleData(from item: HistoryItem) -> QRCodeStyleData? {
|
|
|
// 从历å<EFBFBD>²è®°å½•项䏿<EFBFBD><EFBFBD>å<EFBFBD>–æ ·å¼<EFBFBD>æ•°æ<EFBFBD>®
|
|
|
guard let jsonString = item.qrCodeStyleData,
|
|
|
let jsonData = jsonString.data(using: .utf8) else {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
do {
|
|
|
let styleData = try JSONDecoder().decode(QRCodeStyleData.self, from: jsonData)
|
|
|
return styleData
|
|
|
} catch {
|
|
|
print("â<EFBFBD>Œ æ ·å¼<C3A5>æ•°æ<C2B0>®JSONè§£ç <C3A7>失败:\(error)")
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - 清空历å<EFBFBD>²è®°å½•确认视图
|
|
|
struct ClearHistoryConfirmView: View {
|
|
|
@EnvironmentObject var languageManager: LanguageManager
|
|
|
@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("clear_history".localized)
|
|
|
.font(.title2)
|
|
|
.fontWeight(.bold)
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
|
|
|
// 简å<EFBFBD>•说明
|
|
|
Text("clear_history_warning".localized)
|
|
|
.font(.body)
|
|
|
.foregroundColor(.secondary)
|
|
|
.multilineTextAlignment(.center)
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
// 按钮区域
|
|
|
VStack(spacing: 12) {
|
|
|
// ç¡®è®¤åˆ é™¤æŒ‰é’®
|
|
|
Button(action: {
|
|
|
onConfirm()
|
|
|
isPresented = false
|
|
|
}) {
|
|
|
HStack {
|
|
|
Image(systemName: "trash.fill")
|
|
|
Text("confirm_delete".localized)
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
}
|
|
|
.frame(maxWidth: .infinity)
|
|
|
.padding()
|
|
|
.background(Color.red)
|
|
|
.foregroundColor(.white)
|
|
|
.cornerRadius(10)
|
|
|
}
|
|
|
|
|
|
// å<EFBFBD>–消按钮
|
|
|
Button(action: {
|
|
|
isPresented = false
|
|
|
}) {
|
|
|
Text("cancel".localized)
|
|
|
.frame(maxWidth: .infinity)
|
|
|
.padding()
|
|
|
.background(Color(.systemGray5))
|
|
|
.foregroundColor(.primary)
|
|
|
.cornerRadius(10)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
.padding(20)
|
|
|
.navigationTitle("confirm_delete".localized)
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
|
.navigationBarBackButtonHidden(true)
|
|
|
.toolbar {
|
|
|
ToolbarItem(placement: .navigationBarLeading) {
|
|
|
Button("close".localized) {
|
|
|
isPresented = false
|
|
|
}
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|