This commit is contained in:
csj
2026-09-12 22:44:53 +08:00
parent 467e374bf6
commit 45fda4bd6b
4 changed files with 52 additions and 3 deletions
+6
View File
@@ -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 导出保持原样。
## 使用
+18 -1
View File
@@ -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
+6 -1
View File
@@ -298,9 +298,14 @@ struct AdminView: View {
.clipShape(RoundedRectangle(cornerRadius: 8))
}
Divider()
ForEach(record.files.values.sorted(by: { $0.kind < $1.kind })) { file in
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)
+21
View File
@@ -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")
}
}