import request from '@/utils/request' /** * 妇幼档案接口(妇科 / 儿科) * 文档:《妇保接口文档》2026-10-11(共 8 个接口) * 库表: * 妇科主表 pat_maternity_archive /smartor/maternityArchive * 妇科详情 pat_maternity_archive_detail /smartor/maternityArchiveDetail * 儿科主表 pat_child_archive /smartor/childArchive * 儿科详情 pat_child_archive_detail /smartor/childArchiveDetail * * 与旧版的差异(重点): * 1. 新增 / 修改合并为一个接口 insertOrUpdateXxx,请求体是「主表字段 + 详情表字段」扁平混合, * 一次调用同时维护两张表,返回 data 为主表 ID(旧版 add 不返回主键,需要反查)。 * 2. 删除改为 POST batchDelete,按主表 ID 批量删,详情表在同一事务里逻辑删除。 * 3. 详情查询 GET /xxxDetail/getInfo/{archiveid},路径参数是【主表 ID】, * 返回的那条详情记录里:data.id 是【详情表主键】,data.archiveid 是【主表 ID】。 * * ⚠️ id 与 archiveid 是两个独立字段,任何时候都不能互相赋值: * insertOrUpdate 请求体里 id = 详情表主键,archiveid = 主表 ID; * 主表行只有 id(列表接口不返回 archiveid),详情行 id 与 archiveid 同时存在。 * 判断一行是主表行还是详情行,必须由调用方显式声明,不能靠「有没有 archiveid」猜。 * * 通用约定: * 1. 请求体 Content-Type: application/json。 * 2. 日期字段统一 yyyy-MM-dd HH:mm:ss。 * 3. 建档时间范围用 params.beginTime / params.endTime(yyyy-MM-dd)。 * 4. orgid / campusid 由后端按登录用户自动注入,前端不用传。 * 5. 量表 / 可编辑表格数据存详情表 TEXT 字段(JSON 字符串)。 */ /* ==================== 模块路径 ==================== */ const BASE = { maternity: '/smartor/maternityArchive', maternityDetail: '/smartor/maternityArchiveDetail', child: '/smartor/childArchive', childDetail: '/smartor/childArchiveDetail' } /* ==================== 档案类型编码 ==================== * 注意:妇科与儿科各自从 1 开始编号,不能跨科混用。 * 妇科:1-早孕关爱 2-围绝经期MHI * 儿科:1-营养性疾病 2-学习困难 3-高危儿专案 * key 与 views/patient/archives/types.js 的 ARCHIVE_TYPES[].key 保持一致 */ export const ARCHIVE_TYPE_CODE = { pregnancy: { module: 'maternity', archiveType: '1', label: '早孕关爱' }, menopause: { module: 'maternity', archiveType: '2', label: '围绝经期MHI' }, nutrition: { module: 'child', archiveType: '1', label: '营养性疾病' }, learning: { module: 'child', archiveType: '2', label: '学习困难' }, highrisk: { module: 'child', archiveType: '3', label: '高危儿专案' } } // 主表模块 -> 详情表模块 const DETAIL_MODULE = { maternity: 'maternityDetail', child: 'childDetail' } /** * 由专案 key 解析出所属模块、档案类型编码与接口前缀 * @param {String} typeKey pregnancy|menopause|nutrition|learning|highrisk */ export function resolveTarget(typeKey) { const t = ARCHIVE_TYPE_CODE[typeKey] if (!t) return null const detailModule = DETAIL_MODULE[t.module] return { ...t, detailModule, base: BASE[t.module], detailBase: BASE[detailModule] } } /** * 反查:模块 + 档案类型编码 -> 专案 key * @param {String} module maternity|child * @param {String|Number} archiveType 1|2|3 */ export function resolveKey(module, archiveType) { const keys = Object.keys(ARCHIVE_TYPE_CODE) for (let i = 0; i < keys.length; i++) { const t = ARCHIVE_TYPE_CODE[keys[i]] if (t.module === module && String(t.archiveType) === String(archiveType)) return keys[i] } return '' } /* ==================== 日期工具 ==================== */ /** 当前时间 yyyy-MM-dd HH:mm:ss */ export function nowDateTime() { const d = new Date() const p = (n) => String(n).padStart(2, '0') return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}` } /** 今天 yyyy-MM-dd */ export function today() { const d = new Date() const p = (n) => String(n).padStart(2, '0') return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}` } /** 日期补成 yyyy-MM-dd HH:mm:ss(后端 DATETIME 列要求完整时间格式) */ export function toDateTime(val) { if (!val) return '' // 兼容 2026/08/25 这类斜杠格式 const s = String(val).trim().replace('T', ' ').replace(/\//g, '-') // 只有日期:2026-08-25 -> 2026-08-25 00:00:00 if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s + ' 00:00:00' // 缺秒:2026-08-25 09:30 -> 2026-08-25 09:30:00 if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(s)) return s + ':00' return s.substring(0, 19) } /** 需要补全时分秒的日期字段 */ export const DATE_FIELDS = [ 'birthdate', 'diagTime', 'archiveTime', 'plannedFollowTime', 'dueDate', 'lastMenstrualTime' ] /** 把 payload 里的日期字段统一补成 yyyy-MM-dd HH:mm:ss */ export function normalizeDates(payload = {}) { DATE_FIELDS.forEach((k) => { if (payload[k]) payload[k] = toDateTime(payload[k]) }) return payload } /** 由出生日期算年龄 */ export function calcAge(birth) { if (!birth) return '' const b = new Date(String(birth).substring(0, 10).replace(/-/g, '/')) if (isNaN(b.getTime())) return '' const now = new Date() let age = now.getFullYear() - b.getFullYear() const m = now.getMonth() - b.getMonth() if (m < 0 || (m === 0 && now.getDate() < b.getDate())) age-- return age < 0 ? '' : age } /* ==================== 详情表 TEXT 字段(存 JSON) ==================== * 前端页面按「专案 + tab/区块」把对象或数组塞进对应字段 */ export const FORM_FIELD_BY_TAB = { // 早孕关爱 pregnancy: { followupCycle: 'followupPeriod', // 随访周期(EditableTable 数组) followup: 'epEarlyPregnancyFollowupForm' // 早孕随访表 }, // 围绝经期MHI menopause: { sysQ: 'mhiMenopauseFollowupSystemIssuesForm', // 更年期随访系统问题表 mhtFirst: 'mhiMhtInitialVisitMedicalRecordForm', // MHT首诊病历表 mhtFollow: 'mhiMhtFollowUpMedicalRecordForm', // MHT复诊病历表 kupperman: 'mhiModifiedKuppermanScoringScaleForm', // 改良Kupperman评分标准表 mrs: 'mhiMenopauseRatingScaleForm', // 更年期评定量表(MRS) psqi: 'mhiPittsburghSleepQualityIndexForm' // 匹兹堡睡眠质量指数量表(PSQI) }, // 高危儿专案 highrisk: { growth: 'hrcGrowthDevelopmentMonitoring', // 生长发育监测 neuro: 'hrcNeurologicalEvaluation', // 神经系统评估 intervention: 'hrcInterventionRecords' // 干预记录 }, // 营养性疾病 nutrition: { measure: 'ndPhysicalMeasurements', // 体格测量 diet: 'ndDietaryAssessment', // 膳食评估 plan: 'ndInterventionProgram' // 干预方案 }, // 学习困难(页面上是一整个 form,落库需拆成 4 个 TEXT 字段) learning: { appointment: 'appointmentSituation', // 预约情况(archive.appointmentStatus) ability: 'ldLearningAbilityAssessment', // 学习能力评估 behavior: 'ldBehavioralAssessment', // 行为评估 train: 'ldInterventionTrainingRecord' // 干预训练记录 } } /* ==================== 字段对照(前端 -> 后端) ==================== * 前端现有档案对象沿用 views/patient/archives/types.js 的命名, * 与后端 VO 驼峰命名不一致,提交/回显时用 toApiFields / fromApiFields 转换。 * 注意:前端 age 字段后端无对应列(由 birthdate 推算),不参与提交。 */ export const FIELD_MAP = { // 患者 / 档案通用 patid: 'patid', patname: 'patname', sex: 'sex', telcode: 'phone', idcardno: 'idcardno', birthday: 'birthdate', diagnosisDate: 'diagTime', createTime: 'archiveTime', outhospno: 'outhospno', inhospno: 'inhospno', serialnum: 'serialnum', deptcode: 'deptcode', deptname: 'deptname', wardcode: 'wardcode', wardname: 'wardname', icd10code: 'icd10code', diagname: 'diagname', // 医生与登记 attendingDoctor: 'doctorName', managingDoctor: 'managementDoctorName', registrant: 'registerUser', closeStatus: 'closeStatus', // 妇科 pregnancyStatus: 'pregnancySituation', followupCycles: 'followupCycleTimes', lmp: 'lastMenstrualTime', // 儿科 birthWeight: 'birthWeight', gestationalWeek: 'gestationalWeek', highriskCategory: 'highRiskChildType', highriskName: 'highRiskChildName', nutritionCode: 'nutritionIcd10Code', nutritionName: 'nutritionDiagName', appointmentStatus: 'appointmentSituation', followupDate: 'plannedFollowTime' } // 家长 / 监护人(前端 parentInfo 子对象 -> 详情表字段) export const PARENT_FIELD_MAP = { parentName: 'parentname', relation: 'parentRelation', parentPhone: 'parentPhone', parentIdCard: 'parentIdcardno', address: 'parentAddress' } /** 按 FIELD_MAP 把前端对象转成后端字段(只转换有值的键) */ export function toApiFields(obj = {}) { const out = {} Object.keys(FIELD_MAP).forEach((k) => { const v = obj[k] if (v !== undefined && v !== null && v !== '') out[FIELD_MAP[k]] = v }) return out } /** 按 FIELD_MAP 反向把后端行数据转成前端字段(保留未映射字段) */ export function fromApiFields(row = {}) { const reverse = {} Object.keys(FIELD_MAP).forEach((k) => { reverse[FIELD_MAP[k]] = k }) const out = { ...row } Object.keys(row).forEach((k) => { if (reverse[k] && reverse[k] !== k) { out[reverse[k]] = row[k] } }) return out } /** 家长信息转换 */ export function toApiParent(parentInfo = {}) { const out = {} Object.keys(PARENT_FIELD_MAP).forEach((k) => { const v = parentInfo[k] if (v !== undefined && v !== null && v !== '') out[PARENT_FIELD_MAP[k]] = v }) return out } /* ==================== JSON 打包 / 解包 ==================== * 后端 TEXT 字段存 JSON 字符串,前端统一用这两个方法出入 */ export function packJson(value) { if (value === null || value === undefined) return '' if (typeof value === 'string') return value try { return JSON.stringify(value) } catch (e) { return '' } } export function unpackJson(text, fallback) { const def = fallback === undefined ? {} : fallback if (!text) return def if (typeof text !== 'string') return text try { return JSON.parse(text) } catch (e) { return def } } /** * 学习困难:页面上一整个 form 拆成后端 4 个 TEXT 字段 * @param {Object} form 页面 form(学习能力评估 / 行为评估 / 干预训练记录 混在一起) * @param {String} appointmentStatus 预约情况 */ export function splitLearningForm(form = {}, appointmentStatus = '') { const f = FORM_FIELD_BY_TAB.learning return { [f.appointment]: packJson({ appointmentStatus: appointmentStatus }), [f.ability]: packJson({ assessDate: form.assessDate, testName: form.testName, iq: form.iq, reading: form.reading, writing: form.writing, math: form.math, result: form.result }), [f.behavior]: packJson({ behaviorDate: form.behaviorDate, attention: form.attention, hyperactivity: form.hyperactivity, impulsivity: form.impulsivity, emotional: form.emotional, totalScore: form.totalScore, evaluation: form.evaluation }), [f.train]: packJson({ trainDate: form.trainDate, trainingType: form.trainingType, duration: form.duration, trainContent: form.trainContent, trainer: form.trainer, progress: form.progress }) } } /** * 列表查询:自动补上档案类型编码 * @param {String} typeKey 专案 key * @param {Object} params 其它查询条件 */ export function buildListQuery(typeKey, params = {}) { const t = resolveTarget(typeKey) const q = { ...params } if (t && q.archiveType == null) q.archiveType = t.archiveType return q } /* ==================== 一、妇科档案 ==================== */ /** 查询妇科档案列表 POST /selectPatMaternityArchiveList */ export function listMaternityArchive(params) { return request({ url: BASE.maternity + '/selectPatMaternityArchiveList', method: 'post', data: params }) } /** * 新增或修改妇科档案 POST /insertOrUpdateMaternityArchive * 请求体为主表 + 详情表字段扁平混合;返回 data 为妇科档案主表 ID */ export function saveMaternityArchive(data) { return request({ url: BASE.maternity + '/insertOrUpdateMaternityArchive', method: 'post', data: data }) } /** 批量删除妇科档案(含详情)POST /batchDelete,body 为主表 ID 数组 */ export function batchDeleteMaternityArchive(ids) { return request({ url: BASE.maternity + '/batchDelete', method: 'post', data: Array.isArray(ids) ? ids : String(ids).split(',') }) } /** 获取妇科档案详情 GET /maternityArchiveDetail/getInfo/{archiveid}(archiveid 为主表 ID) */ export function getMaternityArchiveDetail(archiveId) { return request({ url: BASE.maternityDetail + '/getInfo/' + archiveId, method: 'get' }) } /* ==================== 二、儿科档案 ==================== */ /** 查询儿科档案列表 POST /selectPatChildArchiveList */ export function listChildArchive(params) { return request({ url: BASE.child + '/selectPatChildArchiveList', method: 'post', data: params }) } /** * 新增或修改儿科档案 POST /insertOrUpdateChildArchive * 请求体为主表 + 详情表字段扁平混合;返回 data 为儿科档案主表 ID */ export function saveChildArchive(data) { return request({ url: BASE.child + '/insertOrUpdateChildArchive', method: 'post', data: data }) } /** 批量删除儿科档案(含详情)POST /batchDelete,body 为主表 ID 数组 */ export function batchDeleteChildArchive(ids) { return request({ url: BASE.child + '/batchDelete', method: 'post', data: Array.isArray(ids) ? ids : String(ids).split(',') }) } /** 获取儿科档案详情 GET /childArchiveDetail/getInfo/{archiveid}(archiveid 为主表 ID) */ export function getChildArchiveDetail(archiveId) { return request({ url: BASE.childDetail + '/getInfo/' + archiveId, method: 'get' }) } /* ==================== 按专案聚合的调用入口 ==================== * 页面不想判断妇科 / 儿科时,直接用这一组 * 例:archiveApi('pregnancy').list({ pageNum: 1, pageSize: 10 }) * archiveApi('pregnancy').save(payload) // 新增或更新,返回主表 ID */ export function archiveApi(typeKey) { const t = resolveTarget(typeKey) if (!t) throw new Error('未知的档案专案类型:' + typeKey) const isChild = t.module === 'child' return { target: t, list: isChild ? listChildArchive : listMaternityArchive, save: isChild ? saveChildArchive : saveMaternityArchive, batchDelete: isChild ? batchDeleteChildArchive : batchDeleteMaternityArchive, getDetail: isChild ? getChildArchiveDetail : getMaternityArchiveDetail } }