WXL (wul)
3 天以前 c70f5e1d471ddf002ef0ebd5f4c6a5ed1c865d0d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
/**
 * 建档模块 接口适配层(2026-10-11 新版接口)
 * ------------------------------------------------------------------
 * 新版后端把「新增 / 修改」合并成一个 insertOrUpdate 接口,请求体是
 * 主表字段 + 详情表字段的扁平混合,一次调用同时维护两张表,并返回主表 ID。
 * 因此这里不再做「主表一次请求 + 详情表一次请求」的拆分,只负责:
 *   1. 前端单一档案对象 <-> 后端扁平字段的转换
 *   2. 量表 / 可编辑表格数据的 JSON 打包与解包
 *
 * 对外函数签名与旧版保持一致,页面无需改动。
 */
 
import {
  ARCHIVE_TYPE_CODE,
  resolveTarget,
  resolveKey,
  toApiFields,
  fromApiFields,
  toApiParent,
  packJson,
  unpackJson,
  splitLearningForm,
  normalizeDates,
  FORM_FIELD_BY_TAB,
  archiveApi,
  nowDateTime,
  today,
  toDateTime,
  calcAge,
} from "@/api/patient/archive";
import { getTypeByKey } from "./types";
 
/* ==================== 内部工具 ==================== */
 
function pick(obj, keys) {
  const out = {};
  keys.forEach((k) => {
    const v = obj && obj[k];
    if (v !== undefined && v !== null && v !== "") out[k] = v;
  });
  return out;
}
 
/** 后端字段 -> 前端字段(列表行、详情记录共用)
 * 注意:appointmentSituation 不在这里,它存的是 JSON 串,由 parseAppointmentStatus 单独解析
 */
const API_TO_FRONT = {
  highRiskChildType: "highriskCategory",
  highRiskChildName: "highriskName",
  nutritionIcd10Code: "nutritionCode",
  nutritionDiagName: "nutritionName",
  plannedFollowTime: "followupDate",
  birthWeight: "birthWeight",
  gestationalWeek: "gestationalWeek",
  pregnancySituation: "pregnancyStatus",
  followupCycleTimes: "followupCycles",
};
 
/**
 * 解析「预约情况」
 * 后端这个字段存的是 JSON 串:{"appointmentStatus":"无预约"},
 * 直接铺到列表上会把整串 JSON 显示出来,这里统一取里面的 appointmentStatus。
 * 已经是纯文本时原样返回,解析不出内容则返回空。
 */
function parseAppointmentStatus(val) {
  if (val === undefined || val === null || val === "") return "";
  if (typeof val !== "string") return val.appointmentStatus || "";
  const s = val.trim();
  if (s.charAt(0) !== "{") return s;
  try {
    const o = JSON.parse(s);
    return (o && o.appointmentStatus) || "";
  } catch (e) {
    return "";
  }
}
 
/* ------------------------------------------------------------------
 * 模块名口径不一致的统一入口
 * 页面(types.js)用 maternal|child,接口层(ARCHIVE_TYPE_CODE)用 maternity|child。
 * 两边不统一会导致 resolveKey 匹配不上,列表页所有专案都识别不出来。
 */
function innerModule(module) {
  return module === "child" ? "child" : "maternity";
}
 
/** 接口层模块名 -> 页面口径 */
function frontModule(module) {
  return module === "child" ? "child" : "maternal";
}
 
/** 详情表冗余的患者基本信息 -> 前端对象 */
function applyCommonFields(target, row) {
  const front = fromApiFields(row);
  Object.keys(front).forEach((k) => {
    // id / detailId / subType 属于骨架自身的字段,不能被原始行数据覆盖
    if (k === "id" || k === "detailId" || k === "subType") return;
    const v = front[k];
    if (v !== undefined && v !== null && v !== "") target[k] = v;
  });
  Object.keys(API_TO_FRONT).forEach((apiKey) => {
    const v = row[apiKey];
    if (v !== undefined && v !== null && v !== "") target[API_TO_FRONT[apiKey]] = v;
  });
  if (row.appointmentSituation) {
    target.appointmentStatus = parseAppointmentStatus(row.appointmentSituation);
  }
  if (row.birthdate) target.age = calcAge(row.birthdate);
  if (row.parentname || row.parentPhone || row.parentRelation) {
    target.parentInfo = Object.assign({}, target.parentInfo, {
      parentName: row.parentname || "",
      relation: row.parentRelation || "",
      parentPhone: row.parentPhone || "",
      parentIdCard: row.parentIdcardno || "",
      address: row.parentAddress || "",
    });
  }
  if (row.sourceType === "1" || row.sourceType === 1) target.sourcePage = "outpatient";
  return target;
}
 
/**
 * 后端行 -> 前端档案对象骨架
 *
 * ⚠️ id 与 archiveid 是两个独立字段,不能互相赋值,必须由调用方显式声明这一行来自哪张表:
 *   from='main'   主表行(列表接口返回):row.id 是主表主键,没有 archiveid
 *   from='detail' 详情行(详情接口返回):row.id 是详情表主键,row.archiveid 是主表 ID
 *
 * 前端对象上:
 *   archive.id       永远 = 主表主键
 *   archive.detailId 永远 = 详情表主键(只有详情行才有)
 *
 * @param {Object} row 后端行
 * @param {String} typeKey 专案 key
 * @param {String} module maternity|child
 * @param {String} from main|detail 这一行的来源表
 */
function skeletonFrom(row, typeKey, module, from) {
  const t = ARCHIVE_TYPE_CODE[typeKey] || {};
  const typeCfg = getTypeByKey(typeKey) || {};
  const isDetail = from === "detail";
  const archive = {
    // 主表主键:主表行直接取 row.id;详情行取外键 row.archiveid
    id: isDetail ? row.archiveid : row.id,
    // 详情表主键:只有详情行才有,主表行(列表)拿不到,保持为空
    detailId: isDetail ? row.id : "",
    subType: typeKey,
    subTypeLabel: typeCfg.subTypeLabel || t.label || "",
    mainCategory: frontModule(module),
    mainCategoryLabel: frontModule(module) === "child" ? "儿保科建档" : "妇保科建档",
    sourcePage: "",
    parentInfo: {},
    archiveForm: {},
  };
  return applyCommonFields(archive, row);
}
 
/** 列表行(主表行) -> 前端档案对象 */
function normalizeRow(row, module) {
  const inner = innerModule(module);
  const key = resolveKey(inner, row.archiveType) || "";
  return skeletonFrom(row, key, inner, "main");
}
 
/* ------------------------------------------------------------------
 * 主表行缓存
 * 列表接口只返回主表行(不含详情主键),详情接口按主表 ID 查,两者用主表 id 就能串起来。
 * 把列表行按主表 id 暂存到 sessionStorage,getArchive 取不到详情时用它兜底,
 * 至少能把姓名、性别、诊断、联系方式这些主表字段显示出来,而不是一片空白。
 */
const ROW_PREFIX = "archiveRow:";
 
function cacheRows(rows = []) {
  try {
    rows.forEach((r) => {
      // 列表行是主表行,主键只认 row.id;不要用 archiveid 兜底,主表行根本没有这个字段
      const id = r.id;
      if (id) sessionStorage.setItem(ROW_PREFIX + id, JSON.stringify(r));
    });
  } catch (e) {
    /* 忽略隐私模式等写入异常 */
  }
}
 
function readRow(id) {
  if (!id) return null;
  try {
    return JSON.parse(sessionStorage.getItem(ROW_PREFIX + id) || "null");
  } catch (e) {
    return null;
  }
}
 
/* ------------------------------------------------------------------
 * 新建草稿缓存
 * 详情接口按【主表 ID】查询,但 insertOrUpdate 刚返回主表 ID 时后端详情行可能还没落好,
 * 新建完立刻跳详情页有概率取不到详情记录。这里把新建时的档案对象暂存到
 * sessionStorage,getArchive 取不到详情时用它兜底,避免详情页一片空白。
 * 保存成功后清除。
 */
const DRAFT_PREFIX = "archiveDraft:";
 
function saveDraft(id, archive) {
  if (!id) return;
  try {
    sessionStorage.setItem(DRAFT_PREFIX + id, JSON.stringify(archive));
  } catch (e) {
    /* 忽略隐私模式等写入异常 */
  }
}
 
function readDraft(id) {
  if (!id) return null;
  try {
    return JSON.parse(sessionStorage.getItem(DRAFT_PREFIX + id) || "null");
  } catch (e) {
    return null;
  }
}
 
function clearDraft(id) {
  if (!id) return;
  try {
    sessionStorage.removeItem(DRAFT_PREFIX + id);
  } catch (e) {
    /* ignore */
  }
}
 
/* ==================== 前端对象 -> 扁平 payload ==================== */
 
/**
 * 构造 insertOrUpdate 的请求体(主表 + 详情表字段扁平混合)
 * @param {Object} archive 前端档案对象
 * @param {Object} t resolveTarget 结果
 * @param {Boolean} forUpdate 是否更新(带 archiveid / id)
 */
export function buildPayload(archive, t, forUpdate) {
  const key = archive.subType;
  const F = FORM_FIELD_BY_TAB[key] || {};
  const payload = toApiFields(archive);
 
  payload.archiveType = t.archiveType;
  if (forUpdate) {
    // 扁平请求体里这两个字段各自代表一张表的主键,值来源不同,不能互相顶替:
    //   archiveid = 主表 ID(来自 archive.id)
    //   id        = 详情表主键(来自 archive.detailId,列表接口不返回,取不到就不传,后端按新增处理)
    if (archive.id) payload.archiveid = archive.id;
    if (archive.detailId) payload.id = archive.detailId;
  }
  if (archive.sourcePage === "outpatient") payload.sourceType = "1";
 
  // 冗余的患者基本信息:详情表也存一份
  const common = pick(archive, [
    "outhospno", "inhospno", "serialnum", "patid", "patname", "sex",
    "deptcode", "deptname", "wardcode", "wardname",
    "icd10code", "diagname", "idcardno",
  ]);
  Object.assign(payload, common, {
    birthdate: toDateTime(archive.birthday),
    diagTime: toDateTime(archive.diagnosisDate),
    archiveTime: toDateTime(archive.createTime),
    doctorName: archive.attendingDoctor || "",
    managementDoctorName: archive.managingDoctor || "",
    registerUser: archive.registrant || "",
    closeStatus: archive.closeStatus == null ? "0" : String(archive.closeStatus),
  });
 
  if (key === "pregnancy") {
    const p = archive.pregFormData || {};
    const reg = p.registration || {};
    const preg = p.pregnancy || {};
    const fv = p.firstVisit || {};
    Object.assign(payload, {
      lastMenstrualTime: toDateTime(fv.lmp),
      firstDoctorName: fv.firstDoctor || "",
      contactPhone: fv.contactPhone || fv.selfPhone || "",
      address: fv.address || "",
      pregnancySituation: archive.pregnancyStatus || "",
      specificSituation: packJson(preg.details || []),
      handleSuggestion: packJson(preg.hint || []),
      isRecordEstablished: reg.isRegistered === "是" ? "1" : reg.isRegistered === "否" ? "0" : "",
      dueDate: toDateTime(reg.edd),
      followupPeriod: packJson(p.followupCycle || []),
      [F.followup]: packJson(p),
    });
  } else if (key === "menopause") {
    const f = archive.formData || {};
    Object.assign(payload, {
      [F.sysQ]: packJson(f.menoSysQ || {}),
      [F.mhtFirst]: packJson(f.mhtFirst || {}),
      [F.mhtFollow]: packJson(f.mhtFollow || {}),
      [F.kupperman]: packJson(f.kupperman || {}),
      [F.mrs]: packJson(f.mrs || {}),
      [F.psqi]: packJson(f.psqi || {}),
    });
  } else if (key === "highrisk") {
    Object.assign(payload, toApiParent(archive.parentInfo || {}), {
      highRiskChildType: archive.highriskCategory || "",
      highRiskChildName: archive.highriskName || "",
      plannedFollowTime: toDateTime(archive.followupDate),
      [F.growth]: packJson(archive.growthData || []),
      [F.neuro]: packJson(archive.neuroData || []),
      [F.intervention]: packJson(archive.interventionData || []),
    });
  } else if (key === "nutrition") {
    Object.assign(payload, toApiParent(archive.parentInfo || {}), {
      nutritionDiagName: archive.nutritionName || "",
      nutritionIcd10Code: archive.nutritionCode || "",
      [F.measure]: packJson(archive.measureData || []),
      [F.diet]: packJson(archive.dietData || []),
      [F.plan]: packJson(archive.planData || []),
    });
  } else if (key === "learning") {
    const ld =
      (archive.archiveForm && archive.archiveForm.learningData) || archive.archiveForm || {};
    Object.assign(
      payload,
      toApiParent(archive.parentInfo || {}),
      splitLearningForm(ld, archive.appointmentStatus || "")
    );
  }
 
  delete payload.age;
  return normalizeDates(payload);
}
 
/* ==================== 详情记录 -> 页面表单数据 ==================== */
 
/** 把详情表 TEXT 字段还原成页面表单数据 */
function applyDetailToForms(archive, detail) {
  const key = archive.subType;
  const F = FORM_FIELD_BY_TAB[key] || {};
 
  if (key === "pregnancy") {
    const early = unpackJson(detail.epEarlyPregnancyFollowupForm, null);
    archive.pregFormData = early || {};
    if (!archive.pregFormData.followupCycle) {
      archive.pregFormData.followupCycle = unpackJson(detail.followupPeriod, []);
    }
    if (!archive.pregFormData.pregnancy) {
      archive.pregFormData.pregnancy = {
        status: "",
        details: unpackJson(detail.specificSituation, []),
        hint: unpackJson(detail.handleSuggestion, []),
      };
    }
    if (!archive.pregFormData.registration) archive.pregFormData.registration = {};
    if (detail.isRecordEstablished != null && detail.isRecordEstablished !== "") {
      archive.pregFormData.registration.isRegistered =
        detail.isRecordEstablished === "1" ? "是" : "否";
    }
    if (detail.dueDate && !archive.pregFormData.registration.edd) {
      archive.pregFormData.registration.edd = String(detail.dueDate).substring(0, 10);
    }
    if (detail.lastMenstrualTime && !archive.pregFormData.firstVisit) {
      archive.pregFormData.firstVisit = {
        lmp: String(detail.lastMenstrualTime).substring(0, 10),
      };
    }
  } else if (key === "menopause") {
    archive.formData = {
      menoSysQ: unpackJson(detail.mhiMenopauseFollowupSystemIssuesForm, {}),
      mhtFirst: unpackJson(detail.mhiMhtInitialVisitMedicalRecordForm, {}),
      mhtFollow: unpackJson(detail.mhiMhtFollowUpMedicalRecordForm, {}),
      kupperman: unpackJson(detail.mhiModifiedKuppermanScoringScaleForm, {}),
      mrs: unpackJson(detail.mhiMenopauseRatingScaleForm, {}),
      psqi: unpackJson(detail.mhiPittsburghSleepQualityIndexForm, {}),
    };
  } else if (key === "highrisk") {
    archive.growthData = unpackJson(detail.hrcGrowthDevelopmentMonitoring, []);
    archive.neuroData = unpackJson(detail.hrcNeurologicalEvaluation, []);
    archive.interventionData = unpackJson(detail.hrcInterventionRecords, []);
  } else if (key === "nutrition") {
    archive.measureData = unpackJson(detail.ndPhysicalMeasurements, []);
    archive.dietData = unpackJson(detail.ndDietaryAssessment, []);
    archive.planData = unpackJson(detail.ndInterventionProgram, []);
  } else if (key === "learning") {
    const appt = unpackJson(detail.appointmentSituation, {});
    const ability = unpackJson(detail.ldLearningAbilityAssessment, {});
    const behavior = unpackJson(detail.ldBehavioralAssessment, {});
    const train = unpackJson(detail.ldInterventionTrainingRecord, {});
    archive.appointmentStatus = appt.appointmentStatus || "";
    archive.archiveForm = {
      learningData: Object.assign({}, ability, behavior, train),
    };
  }
}
 
/* ==================== 查询 ==================== */
 
/**
 * 分页查询某专案档案(自动带上 archiveType)
 * @param {String} typeKey 专案 key
 * @param {Object} params 查询条件,支持 params.beginTime / params.endTime
 * @returns Promise<{ rows, total }>
 */
export function listArchives(typeKey, params = {}) {
  const t = resolveTarget(typeKey);
  if (!t) return Promise.resolve({ rows: [], total: 0 });
  const api = archiveApi(typeKey);
  const query = Object.assign({}, params, { archiveType: t.archiveType });
  return api.list(query).then((res) => {
    const rows = res.rows || [];
    cacheRows(rows);
    return {
      rows: rows.map((r) => normalizeRow(r, t.module)),
      total: res.total || 0,
    };
  });
}
 
/**
 * 按科室查询全部专案(妇保 / 儿保总列表页用,不带 archiveType)
 * @param {String} module maternal|child
 */
export function listArchivesByModule(module, params = {}) {
  const isChild = innerModule(module) === "child";
  // 两个模块共用一个控制器,取任一专案 key 只是为了拿到接口
  const api = archiveApi(isChild ? "highrisk" : "pregnancy");
  return api.list(Object.assign({}, params)).then((res) => {
    const rows = res.rows || [];
    cacheRows(rows);
    return {
      rows: rows.map((r) => normalizeRow(r, module)),
      total: res.total || 0,
    };
  });
}
 
/**
 * 查询单个档案详情,返回前端完整对象
 * @param {String} typeKey 专案 key
 * @param {String|Number} id 主表 ID(详情接口 getInfo/{archiveid} 就是按它查的)
 * @param {String|Number} [detailId] 已废弃,保留只为兼容旧调用,不再参与请求
 * @returns Promise<前端档案对象 | null>
 */
export function getArchive(typeKey, id, detailId) {
  const t = resolveTarget(typeKey);
  if (!t || !id) return Promise.resolve(null);
  const api = archiveApi(typeKey);
 
  return api
    .getDetail(id)
    .then((res) => {
      const detail = res.data || res;
      if (!detail || !detail.id) return fallbackArchive(typeKey, id, t);
      // 详情记录里的 archiveid 必须等于当前主表 id,否则说明数据串了,宁可不要
      if (detail.archiveid != null && String(detail.archiveid) !== String(id)) {
        console.warn(
          "[archive] 详情记录与主表 id 不匹配,已忽略详情数据:",
          { archiveid: detail.archiveid, expectId: id }
        );
        return fallbackArchive(typeKey, id, t);
      }
      // 详情行:row.id 是详情主键 -> detailId,row.archiveid 是主表 ID -> id
      const archive = skeletonFrom(detail, typeKey, t.module, "detail");
      applyDetailToForms(archive, detail);
      return archive;
    })
    .catch(() => fallbackArchive(typeKey, id, t));
}
 
/** 详情取不到时的兜底:新建草稿 > 主表行缓存 > 仅带主表 id 的空档案 */
function fallbackArchive(typeKey, id, t) {
  const cached = readDraft(id);
  if (cached && cached.subType === typeKey) {
    return Object.assign(emptyArchive(typeKey, id, t), cached, { id });
  }
  const row = readRow(id);
  if (row) return skeletonFrom(row, typeKey, t.module, "main");
  return emptyArchive(typeKey, id, t);
}
 
/** 详情取不到时的兜底:只带主表 id 的空档案,页面仍可填写并保存 */
function emptyArchive(typeKey, id, t) {
  const typeCfg = getTypeByKey(typeKey) || {};
  return {
    id,
    detailId: "",
    subType: typeKey,
    subTypeLabel: typeCfg.subTypeLabel || t.label || "",
    mainCategory: frontModule(t.module),
    mainCategoryLabel: frontModule(t.module) === "child" ? "儿保科建档" : "妇保科建档",
    sourcePage: "",
    parentInfo: {},
    archiveForm: {},
  };
}
 
/* ==================== 新增 ==================== */
 
/**
 * 由一条患者记录(门诊 / 住院 / 独立录入)建档
 * @param {Object} patient 患者记录
 * @param {String} typeKey 专案 key
 * @param {String} sourcePage outpatient|inpatient|standalone
 * @returns Promise<前端档案对象>
 */
export function createArchive(patient, typeKey, sourcePage) {
  const t = resolveTarget(typeKey);
  if (!t) return Promise.reject(new Error("未知的档案专案类型:" + typeKey));
  const api = archiveApi(typeKey);
  const now = nowDateTime();
 
  const draft = {
    subType: typeKey,
    patid: patient.patid || patient.id || "",
    patname: patient.patname || patient.name || "",
    sex: patient.sex,
    telcode: patient.telcode || patient.phone || "",
    idcardno: patient.idcardno || "",
    birthday: patient.birthday || patient.birthdate || patient.goday || "",
    // 门诊/住院记录里诊断日期字段名不统一(admitdate / diagTime / diagnosisDate),且可能只有日期
    diagnosisDate: patient.diagnosisDate || patient.diagTime || patient.admitdate || "",
    createTime: now,
    outhospno: patient.outhospno || "",
    inhospno: patient.inhospno || "",
    serialnum: patient.serialnum || "",
    deptcode: patient.deptcode || "",
    deptname: patient.deptname || "",
    wardcode: patient.wardcode || "",
    wardname: patient.wardname || "",
    icd10code: patient.icd10code || "",
    diagname: patient.diagname || "",
    attendingDoctor: patient.drname || "",
    registrant: localStorage.getItem("YongHuXM") || "",
    closeStatus: "0",
    sourcePage: sourcePage || "outpatient",
    parentInfo: {},
    archiveForm: {},
  };
 
  const payload = buildPayload(draft, t, false);
  return api.save(payload).then((res) => {
    const newId = res && res.data != null ? res.data : "";
    const typeCfg = getTypeByKey(typeKey) || {};
    const archive = Object.assign({}, draft, {
      id: newId,
      detailId: "",
      subTypeLabel: typeCfg.subTypeLabel || t.label,
      mainCategory: frontModule(t.module),
      mainCategoryLabel: frontModule(t.module) === "child" ? "儿保科建档" : "妇保科建档",
      age: calcAge(draft.birthday),
    });
    saveDraft(newId, archive);
    return archive;
  });
}
 
/* ==================== 保存 / 结案 / 删除 ==================== */
 
/**
 * 保存档案:一次 insertOrUpdate 同时更新主表与详情表
 * @param {Object} archive 前端档案对象
 * @param {Boolean} onlyCloseStatus 仅更新结案状态
 */
export function saveArchive(archive, onlyCloseStatus) {
  const key = archive.subType;
  const t = resolveTarget(key);
  if (!t || !archive.id) return Promise.reject(new Error("档案缺少 subType 或 id"));
  const api = archiveApi(key);
 
  if (onlyCloseStatus) {
    return api.save({ archiveid: archive.id, archiveType: t.archiveType, closeStatus: "1" });
  }
  return api.save(buildPayload(archive, t, true)).then((res) => {
    clearDraft(archive.id);
    return res;
  });
}
 
/** 结案 */
export function closeArchive(archive) {
  return saveArchive(archive, true);
}
 
/**
 * 删除档案(主表逻辑删除,详情表在同一事务里一起删)
 * @param {String} typeKey 专案 key
 * @param {Array} ids 主表 id 数组
 */
export function removeArchives(typeKey, ids) {
  const t = resolveTarget(typeKey);
  if (!t || !ids || !ids.length) return Promise.resolve();
  return archiveApi(typeKey).batchDelete(ids);
}
 
/** 删除档案(按行数组,自动按专案分组) */
export function removeArchiveList(rows = []) {
  const groups = {};
  rows.forEach((r) => {
    const key = r.subType || "";
    if (!key) return;
    (groups[key] = groups[key] || []).push(r.id);
  });
  return Promise.all(
    Object.keys(groups).map((key) => removeArchives(key, groups[key]))
  );
}
 
export { calcAge, nowDateTime, today, toDateTime };