diff --git a/PROJECT-ADMIN.md b/PROJECT-ADMIN.md index e2f2c49..e5a49a7 100644 --- a/PROJECT-ADMIN.md +++ b/PROJECT-ADMIN.md @@ -1,5 +1,11 @@ # 管理端接入 +## 多附件兼容 + +项目管理详情现在展示每个材料分类下的全部附件,并支持逐份预览、下载和 ZIP 导出。新版本同时解码旧服务的单附件对象和新服务的附件数组;多附件服务上线时需同步重新构建 Mac 应用。本地 OCR 和 Office 导出不受此次改动影响。 + +独立模型兼容测试:`swiftc reimburse/AdminModels.swift tests/AdminAttachmentsTests.swift -o /tmp/admin-attachments-tests && /tmp/admin-attachments-tests`。 + 应用新增“项目管理”入口,原“本地贴票”入口及 OCR、核对、配对、Office 导出保持原样。 ## 使用 diff --git a/reimburse/AdminModels.swift b/reimburse/AdminModels.swift index 6db3122..abe33f3 100644 --- a/reimburse/AdminModels.swift +++ b/reimburse/AdminModels.swift @@ -35,12 +35,29 @@ struct AdminMember: Codable, Identifiable { struct AdminFile: Codable, Identifiable { let id: String; let name: String; let kind: String; let mime: String; let size: Int64 } +struct AdminAttachmentList: Codable { + let files: [AdminFile] + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let multiple = try? container.decode([AdminFile].self) { + files = multiple + } else { + files = [try container.decode(AdminFile.self)] + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(files) + } +} struct AdminEvent: Codable { let action: String; let actorId: String; let detail: String; let date: String } struct AdminExpense: Codable, Identifiable { let id: String; let groupId: Int64; let projectId: Int64; let userId: String let name: String; let team: String; let projectName: String; let type: String let amountCents: Int64; let amountText: String; let note: String; let status: String; let state: String - let date: String; let version: Int; let files: [String: AdminFile]; let events: [AdminEvent] + let date: String; let version: Int; let files: [String: AdminAttachmentList]; let events: [AdminEvent] } struct AdminExpenseMaterial: Codable, Identifiable { let kind: String diff --git a/reimburse/AdminView.swift b/reimburse/AdminView.swift index afdf183..ef71417 100644 --- a/reimburse/AdminView.swift +++ b/reimburse/AdminView.swift @@ -298,9 +298,14 @@ struct AdminView: View { .clipShape(RoundedRectangle(cornerRadius: 8)) } Divider() - ForEach(record.files.values.sorted(by: { $0.kind < $1.kind })) { file in - AdminAttachmentRow(model: model, file: file) + ScrollView { + VStack(spacing: 12) { + ForEach(record.files.values.flatMap { $0.files }.sorted(by: { ($0.kind, $0.id) < ($1.kind, $1.id) })) { file in + AdminAttachmentRow(model: model, file: file) + } + } } + .frame(maxHeight: 300) ScrollView { ForEach(Array(record.events.enumerated()), id: \.offset) { _, event in HStack(alignment: .top) { Text(event.date).foregroundStyle(.secondary); Text(event.detail.isEmpty ? event.action : event.detail); Spacer(); Text("账户 \(event.actorId)") }.font(.caption).padding(.vertical, 4) diff --git a/tests/AdminAttachmentsTests.swift b/tests/AdminAttachmentsTests.swift new file mode 100644 index 0000000..093329e --- /dev/null +++ b/tests/AdminAttachmentsTests.swift @@ -0,0 +1,21 @@ +import Foundation + +@main +struct AdminAttachmentsTests { + static func main() throws { + let first = #"{"id":"one","name":"first.pdf","kind":"invoice","mime":"application/pdf","size":10}"# + let second = #"{"id":"two","name":"second.pdf","kind":"invoice","mime":"application/pdf","size":20}"# + let decoder = JSONDecoder() + let legacy = try decoder.decode(AdminAttachmentList.self, from: Data(first.utf8)) + precondition(legacy.files.count == 1 && legacy.files[0].id == "one") + let multiple = try decoder.decode(AdminAttachmentList.self, from: Data("[\(first),\(second)]".utf8)) + precondition(multiple.files.map(\.id) == ["one", "two"]) + let roundTrip = try decoder.decode(AdminAttachmentList.self, from: JSONEncoder().encode(multiple)) + precondition(roundTrip.files.map(\.id) == ["one", "two"]) + let empty = try decoder.decode(AdminAttachmentList.self, from: Data("[]".utf8)) + precondition(empty.files.isEmpty) + let mixed = try decoder.decode([String: AdminAttachmentList].self, from: Data("{\"invoice\":[\(first),\(second)],\"legacy\":\(first)}".utf8)) + precondition(mixed.values.flatMap { $0.files }.count == 3) + print("Admin attachment compatibility tests passed") + } +}