1
This commit is contained in:
@@ -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 导出保持原样。
|
应用新增“项目管理”入口,原“本地贴票”入口及 OCR、核对、配对、Office 导出保持原样。
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
|
|||||||
@@ -35,12 +35,29 @@ struct AdminMember: Codable, Identifiable {
|
|||||||
struct AdminFile: Codable, Identifiable {
|
struct AdminFile: Codable, Identifiable {
|
||||||
let id: String; let name: String; let kind: String; let mime: String; let size: Int64
|
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 AdminEvent: Codable { let action: String; let actorId: String; let detail: String; let date: String }
|
||||||
struct AdminExpense: Codable, Identifiable {
|
struct AdminExpense: Codable, Identifiable {
|
||||||
let id: String; let groupId: Int64; let projectId: Int64; let userId: String
|
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 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 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 {
|
struct AdminExpenseMaterial: Codable, Identifiable {
|
||||||
let kind: String
|
let kind: String
|
||||||
|
|||||||
@@ -298,9 +298,14 @@ struct AdminView: View {
|
|||||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
ForEach(record.files.values.sorted(by: { $0.kind < $1.kind })) { file in
|
ScrollView {
|
||||||
AdminAttachmentRow(model: model, file: file)
|
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 {
|
ScrollView {
|
||||||
ForEach(Array(record.events.enumerated()), id: \.offset) { _, event in
|
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)
|
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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user