|
|
@@ -0,0 +1,531 @@
|
|
|
+/**
|
|
|
+ * 老年人营养不良风险评估表 —— Word(.docx) 导出(紧凑段落版,每人一页 A4)
|
|
|
+ *
|
|
|
+ * 格式依据:4.3.18老年人营养评估表-海珠.docx
|
|
|
+ * - 标题「老年人营养不良风险评估表」,右上角为机构名称(仅名称,无“机构名称:”前缀);
|
|
|
+ * - 抬头含 姓名/性别/年龄/床位号/日期/身高/体重/BMI/小腿围/腰围;
|
|
|
+ * - 初筛 6 题 + 评估 14 题,选项以 □/☑ 勾选形式内联排版,与纸质单一致;
|
|
|
+ * - 紧凑排版(8mm 页边距 + 固定行距 + 分档小字号),保证每位长者内容压缩在一张 A4 内,
|
|
|
+ * 下一位长者另起一页。
|
|
|
+ *
|
|
|
+ * 实现:使用项目已有依赖 JSZip 直接组装 OOXML(WordprocessingML),不引入新依赖。
|
|
|
+ */
|
|
|
+import JSZip from 'jszip'
|
|
|
+import { saveAs } from 'file-saver'
|
|
|
+
|
|
|
+/** 导出输入:列表行 + 详情(含 assessData)的合并记录 */
|
|
|
+export interface NutritionExportRecord {
|
|
|
+ /** 机构名称(按 tenantId 解析后的名称) */
|
|
|
+ orgName?: string
|
|
|
+ /** 床位号(列表行 bedName) */
|
|
|
+ bedName?: string
|
|
|
+ /** 详情接口 /elderly-nutritional-risk/get 返回 */
|
|
|
+ detail: any
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------- OOXML 基础工具 ----------------
|
|
|
+
|
|
|
+const FONT_HEI = '黑体'
|
|
|
+const FONT_SONG = '宋体'
|
|
|
+
|
|
|
+/** 字号体系(半磅):标题36 / 机构名24 / 抬头·评分23 / 小节24 / 题目22 / 评定20 */
|
|
|
+const SZ_TITLE = 36
|
|
|
+const SZ_ORG = 24
|
|
|
+const SZ_INFO = 23
|
|
|
+const SZ_SECTION = 24
|
|
|
+const SZ_ITEM = 22
|
|
|
+const SZ_RULE = 20
|
|
|
+
|
|
|
+/** 固定行距(twips,exact 300 ≈ 15pt) */
|
|
|
+const LINE_EXACT = 300
|
|
|
+
|
|
|
+const esc = (s: any): string =>
|
|
|
+ String(s ?? '')
|
|
|
+ .replace(/&/g, '&')
|
|
|
+ .replace(/</g, '<')
|
|
|
+ .replace(/>/g, '>')
|
|
|
+ .replace(/"/g, '"')
|
|
|
+
|
|
|
+interface RunOpts {
|
|
|
+ b?: boolean
|
|
|
+ /** 字号(半磅,如 16 = 8pt) */
|
|
|
+ size?: number
|
|
|
+ font?: string
|
|
|
+}
|
|
|
+
|
|
|
+const run = (text: any, opts: RunOpts = {}): string => {
|
|
|
+ const font = opts.font || FONT_SONG
|
|
|
+ const rPr =
|
|
|
+ `<w:rPr><w:rFonts w:ascii="${font}" w:hAnsi="${font}" w:eastAsia="${font}" w:cs="${font}"/>` +
|
|
|
+ `${opts.b ? '<w:b/><w:bCs/>' : ''}` +
|
|
|
+ `${opts.size ? `<w:sz w:val="${opts.size}"/><w:szCs w:val="${opts.size}"/>` : ''}</w:rPr>`
|
|
|
+ return `<w:r>${rPr}<w:t xml:space="preserve">${esc(text)}</w:t></w:r>`
|
|
|
+}
|
|
|
+
|
|
|
+interface ParaOpts {
|
|
|
+ jc?: 'center' | 'left' | 'right'
|
|
|
+ /** 段前(twips,默认0) */
|
|
|
+ before?: number
|
|
|
+ /** 段后(twips,默认0) */
|
|
|
+ after?: number
|
|
|
+ /** 覆盖固定行距(twips);大字号标题需放宽以免 Word 裁切字面 */
|
|
|
+ line?: number
|
|
|
+}
|
|
|
+
|
|
|
+/** 紧凑段落:固定行距、零段间距(可用 before/after 微调) */
|
|
|
+const para = (runsXml: string, opts: ParaOpts = {}): string => {
|
|
|
+ const pPr =
|
|
|
+ `<w:pPr><w:spacing w:before="${opts.before ?? 0}" w:after="${opts.after ?? 0}" ` +
|
|
|
+ `w:line="${opts.line ?? LINE_EXACT}" w:lineRule="exact"/>` +
|
|
|
+ `${opts.jc ? `<w:jc w:val="${opts.jc}"/>` : ''}</w:pPr>`
|
|
|
+ return `<w:p>${pPr}${runsXml}</w:p>`
|
|
|
+}
|
|
|
+
|
|
|
+const textPara = (text: any, opts: RunOpts & ParaOpts = {}): string => {
|
|
|
+ const { jc, before, after, line, ...runOpts } = opts
|
|
|
+ return para(run(text, runOpts), { jc, before, after, line })
|
|
|
+}
|
|
|
+
|
|
|
+/** 勾选符号:选中 ☑,未选 □ */
|
|
|
+const box = (checked: boolean): string => (checked ? '☑ ' : '□ ')
|
|
|
+
|
|
|
+/** 数字格式化(空值安全) */
|
|
|
+const num = (v: any, digits?: number): string => {
|
|
|
+ if (v === '' || v === null || v === undefined) return ''
|
|
|
+ const n = Number(v)
|
|
|
+ if (isNaN(n)) return String(v)
|
|
|
+ return digits !== undefined ? n.toFixed(digits) : String(n)
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------- 题目定义(内联勾选,顺序/文案与参考 docx 一致) ----------------
|
|
|
+
|
|
|
+interface OptionDef {
|
|
|
+ score: number
|
|
|
+ label: string
|
|
|
+}
|
|
|
+interface ItemDef {
|
|
|
+ /** 题号+题干 */
|
|
|
+ name: string
|
|
|
+ options: OptionDef[]
|
|
|
+ /** 取该题已选分值 */
|
|
|
+ selected: (d: AssessData) => number | undefined
|
|
|
+}
|
|
|
+
|
|
|
+interface AssessData {
|
|
|
+ height?: number
|
|
|
+ weight?: number
|
|
|
+ bmi?: number
|
|
|
+ initialScores?: (number | undefined)[]
|
|
|
+ assessScores?: (number | undefined)[]
|
|
|
+ measureScores?: (number | undefined)[]
|
|
|
+ waistScore?: number
|
|
|
+ calfCircumference?: number
|
|
|
+ waistCircumference?: number
|
|
|
+ initialScore?: number
|
|
|
+ assessScore?: number
|
|
|
+ measureScore?: number
|
|
|
+ ageAdjust?: number
|
|
|
+ totalScore?: number
|
|
|
+ [k: string]: any
|
|
|
+}
|
|
|
+
|
|
|
+// 初筛 6 题(分值数组下标沿用前端表单:initialScores[5]=饮食量、[1]=体重、[2]=活动、[3]=牙齿、[4]=精神、[0]=BMI)
|
|
|
+const INITIAL_ITEMS: ItemDef[] = [
|
|
|
+ {
|
|
|
+ name: '1、近三个月有无饮食量变化:',
|
|
|
+ selected: (d) => d.initialScores?.[5],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: '严重增加或严重减少:0分' },
|
|
|
+ { score: 1, label: '增加或减少:1分' },
|
|
|
+ { score: 2, label: '无变化:2分' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '2、近3个月体重变化:',
|
|
|
+ selected: (d) => d.initialScores?.[1],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: '增加或减少>3公斤:0分' },
|
|
|
+ { score: 1, label: '不知道:1分' },
|
|
|
+ { score: 2, label: '1kg≤减少≤3kg或1kg≤增加≤3kg:2分' },
|
|
|
+ { score: 3, label: '0kg<减少<1kg或0kg<增加<1kg:3分' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '3、活动能力:',
|
|
|
+ selected: (d) => d.initialScores?.[2],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: '卧床:0分' },
|
|
|
+ { score: 1, label: '需要依赖工具活动:1分' },
|
|
|
+ { score: 2, label: '独立户外活动:2分' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '4、牙齿状况:',
|
|
|
+ selected: (d) => d.initialScores?.[3],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: '全口/半口缺:0分' },
|
|
|
+ { score: 1, label: '用义齿:1分' },
|
|
|
+ { score: 2, label: '正常:2分' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '5、精神心理问题:',
|
|
|
+ selected: (d) => d.initialScores?.[4],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: '严重认知障碍或抑郁:0分' },
|
|
|
+ { score: 1, label: '轻度认知障碍或抑郁:1分' },
|
|
|
+ { score: 2, label: '无认知障碍或抑郁:2分' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '6、身体质量指数(BMI)(或小腿围):',
|
|
|
+ selected: (d) => d.initialScores?.[0],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: 'BMI<19或BMI>28:0分' },
|
|
|
+ { score: 1, label: '19≤BMI<21或26<BMI≤28:1分' },
|
|
|
+ { score: 2, label: '21≤BMI<23或24<BMI≤26:2分' },
|
|
|
+ { score: 3, label: '23≤BMI≤24:3分' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+]
|
|
|
+
|
|
|
+// 评估题(顺序按参考 docx;分值数组下标沿用前端表单 assessScores/measureScores/waistScore)
|
|
|
+const ASSESS_ITEMS: ItemDef[] = [
|
|
|
+ {
|
|
|
+ name: '7、患慢性病数>3种:',
|
|
|
+ selected: (d) => d.assessScores?.[0],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: '是:0分' },
|
|
|
+ { score: 1, label: '否:1分' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '8、服药时间在一个月以上的药物种类>3种:',
|
|
|
+ selected: (d) => d.assessScores?.[1],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: '是:0分' },
|
|
|
+ { score: 1, label: '否:1分' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '9、是否独居:',
|
|
|
+ selected: (d) => d.assessScores?.[2],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: '是:0分' },
|
|
|
+ { score: 1, label: '否:1分' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '10、睡眠时间:',
|
|
|
+ selected: (d) => d.assessScores?.[3],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: '<5 h/d:0分' },
|
|
|
+ { score: 1, label: '≥5 h/d:1分' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '11、户外独立活动时间:',
|
|
|
+ selected: (d) => d.assessScores?.[4],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: '<1 h/d:0分' },
|
|
|
+ { score: 1, label: '≥1 h/d:1分' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '12、每天摄入奶类;每天摄入豆制品;每天摄入鱼/肉/禽/蛋类食品:',
|
|
|
+ selected: (d) => d.assessScores?.[9],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: '0-1项:0分' },
|
|
|
+ { score: 0.5, label: '2项:0.5分' },
|
|
|
+ { score: 1, label: '3项:1分' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '13、文化程度:',
|
|
|
+ selected: (d) => d.assessScores?.[5],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: '小学及以下:0分' },
|
|
|
+ { score: 1, label: '中学及以上:1分' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '14、进食能力:',
|
|
|
+ selected: (d) => d.assessScores?.[7],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: '依靠别人:0分' },
|
|
|
+ { score: 1, label: '自行进食稍有困难:1分' },
|
|
|
+ { score: 2, label: '自行进食:2分' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '15、一天餐次:',
|
|
|
+ selected: (d) => d.assessScores?.[8],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: '1次:0分' },
|
|
|
+ { score: 1, label: '2次:1分' },
|
|
|
+ { score: 2, label: '3次及以上:2分' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '16、自我感觉经济状况:',
|
|
|
+ selected: (d) => d.assessScores?.[6],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: '差:0分' },
|
|
|
+ { score: 0.5, label: '一般:0.5分' },
|
|
|
+ { score: 1, label: '良好:1分' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '17、每天烹调油摄入量:',
|
|
|
+ selected: (d) => d.assessScores?.[10],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: '>25g:0分' },
|
|
|
+ { score: 1, label: '≤25g:1分' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '18、是否每天吃蔬菜水果500g及以上:',
|
|
|
+ selected: (d) => d.measureScores?.[0],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: '否:0分' },
|
|
|
+ { score: 1, label: '是:1分' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '19、小腿围:',
|
|
|
+ selected: (d) => d.measureScores?.[1],
|
|
|
+ options: [
|
|
|
+ { score: 0, label: '<31cm:0分' },
|
|
|
+ { score: 1, label: '≥31cm:1分' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+]
|
|
|
+
|
|
|
+const RESULT_OPTIONS = [
|
|
|
+ { level: 'good', label: '营养正常' },
|
|
|
+ { level: 'at_risk', label: '存在营养不良的风险' },
|
|
|
+ { level: 'malnutrition', label: '营养不良' }
|
|
|
+]
|
|
|
+
|
|
|
+const RESULT_RULES = [
|
|
|
+ 'a) 若初筛总分≥12分提示无营养不良风险,无需评估;',
|
|
|
+ 'b) 若初筛总分<12分提示有营养不良风险,继续评估;',
|
|
|
+ 'c) 若营养不良风险评估总分(初筛+评估)≥24分,表示营养状况良好;',
|
|
|
+ 'd) 若营养不良风险评估总分(初筛+评估)<24分,当BMI≥24(或男性腰围≥90cm,女性腰围≥80cm)时,提示可能是肥胖/超重型营养不良或有营养不良风险;',
|
|
|
+ 'e) 若营养不良风险评估总分(初筛+评估)17分~24分,表示有营养不良风险;',
|
|
|
+ 'f) 若营养不良风险评估总分(初筛+评估)≤17分,表示有营养不良。'
|
|
|
+]
|
|
|
+
|
|
|
+// ---------------- 单组长者内容渲染(紧凑段落式) ----------------
|
|
|
+
|
|
|
+const normSex = (v: any): string => {
|
|
|
+ if (v === 1 || v === '1') return '男'
|
|
|
+ if (v === 2 || v === '2') return '女'
|
|
|
+ return String(v ?? '')
|
|
|
+}
|
|
|
+
|
|
|
+const itemPara = (item: ItemDef, d: AssessData): string => {
|
|
|
+ const selected = item.selected(d)
|
|
|
+ const runs =
|
|
|
+ run(item.name, { b: true, size: SZ_ITEM }) +
|
|
|
+ item.options.map((o) => run(`${box(selected === o.score)}${o.label} `, { size: SZ_ITEM })).join('')
|
|
|
+ return para(runs)
|
|
|
+}
|
|
|
+
|
|
|
+const renderElderBlock = (record: NutritionExportRecord): string => {
|
|
|
+ const res: any = record.detail || {}
|
|
|
+ let d: AssessData = {}
|
|
|
+ try {
|
|
|
+ d = res.assessData ? JSON.parse(res.assessData) : {}
|
|
|
+ } catch (e) {
|
|
|
+ d = {}
|
|
|
+ }
|
|
|
+
|
|
|
+ const initialScore = d.initialScore ?? res.initialScore ?? 0
|
|
|
+ const assessScore = d.assessScore ?? res.assessScore ?? 0
|
|
|
+ const measureScore = d.measureScore ?? res.measureScore ?? 0
|
|
|
+ const ageAdjust = d.ageAdjust ?? res.ageAdjust ?? 0
|
|
|
+ const totalScore = d.totalScore ?? res.assessScore ?? 0
|
|
|
+ const riskLevel = res.riskLevel || d.riskLevel || ''
|
|
|
+
|
|
|
+ const parts: string[] = []
|
|
|
+
|
|
|
+ // 机构名称:右上角仅显示名称(无“机构名称:”前缀),为空则不占位
|
|
|
+ if (record.orgName) {
|
|
|
+ parts.push(textPara(record.orgName, { font: FONT_HEI, b: true, size: SZ_ORG, jc: 'right', line: 360 }))
|
|
|
+ }
|
|
|
+ // 标题
|
|
|
+ parts.push(
|
|
|
+ textPara('老年人营养不良风险评估表', {
|
|
|
+ font: FONT_HEI,
|
|
|
+ b: true,
|
|
|
+ size: SZ_TITLE,
|
|
|
+ jc: 'center',
|
|
|
+ after: 60,
|
|
|
+ line: 520
|
|
|
+ })
|
|
|
+ )
|
|
|
+
|
|
|
+ // 抬头信息(两行排满,压缩纵向空间;测量值有值才带单位)
|
|
|
+ parts.push(
|
|
|
+ para(
|
|
|
+ run(
|
|
|
+ `姓名:${res.elderName || ''} 性别:${normSex(res.elderSex)} 年龄:${res.elderAge ?? ''} 床位号:${record.bedName || ''}`,
|
|
|
+ { size: SZ_INFO }
|
|
|
+ )
|
|
|
+ )
|
|
|
+ )
|
|
|
+ const measureLine = [
|
|
|
+ res.assessDate ? `日期:${res.assessDate}` : '',
|
|
|
+ d.height !== undefined && d.height !== null && d.height !== '' ? `身高:${num(d.height, 2)}m` : '',
|
|
|
+ d.weight !== undefined && d.weight !== null && d.weight !== '' ? `体重:${num(d.weight, 1)}kg` : '',
|
|
|
+ d.bmi ? `BMI:${num(d.bmi, 1)}kg/㎡` : '',
|
|
|
+ d.calfCircumference !== undefined && d.calfCircumference !== null && d.calfCircumference !== ''
|
|
|
+ ? `小腿围:${num(d.calfCircumference, 1)}cm`
|
|
|
+ : '',
|
|
|
+ d.waistCircumference !== undefined && d.waistCircumference !== null && d.waistCircumference !== ''
|
|
|
+ ? `腰围:${num(d.waistCircumference, 1)}cm`
|
|
|
+ : ''
|
|
|
+ ]
|
|
|
+ .filter(Boolean)
|
|
|
+ .join(' ')
|
|
|
+ if (measureLine) parts.push(para(run(measureLine, { size: SZ_INFO }), { after: 40 }))
|
|
|
+
|
|
|
+ // 初筛
|
|
|
+ parts.push(textPara('初筛:', { font: FONT_HEI, b: true, size: SZ_SECTION, before: 30 }))
|
|
|
+ INITIAL_ITEMS.forEach((item) => parts.push(itemPara(item, d)))
|
|
|
+ parts.push(
|
|
|
+ textPara(
|
|
|
+ '总分14分,<12分提示有营养不良风险,继续以下评估;≥12分提示无营养不良风险,无需以下评估。',
|
|
|
+ { size: SZ_ITEM, after: 30 }
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ // 评估
|
|
|
+ parts.push(textPara('评估:', { font: FONT_HEI, b: true, size: SZ_SECTION, before: 30 }))
|
|
|
+ ASSESS_ITEMS.forEach((item) => parts.push(itemPara(item, d)))
|
|
|
+
|
|
|
+ // 腰围(男女同行,waistScore:0=男>90 / 1=男≤90 / 2=女>80 / 3=女≤80)
|
|
|
+ const w = d.waistScore
|
|
|
+ parts.push(
|
|
|
+ para(
|
|
|
+ run('20、腰围:', { b: true, size: SZ_ITEM }) +
|
|
|
+ run(
|
|
|
+ `男性 ${box(w === 0)}>90cm:0分 ${box(w === 1)}≤90cm:1分 女性 ${box(w === 2)}>80cm:0分 ${box(w === 3)}≤80cm:1分`,
|
|
|
+ { size: SZ_ITEM }
|
|
|
+ )
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ // 评分说明
|
|
|
+ parts.push(
|
|
|
+ textPara(
|
|
|
+ '评估总分30分,≤17分提示营养不良;17-24分提示有营养不良的风险;≥24分营养正常。',
|
|
|
+ { size: SZ_ITEM, before: 30 }
|
|
|
+ )
|
|
|
+ )
|
|
|
+ parts.push(
|
|
|
+ textPara(
|
|
|
+ `年龄超过70岁总分加1分,即年龄调整增加的分值:${ageAdjust}分,年龄<70岁:0分,年龄≥70岁:1分`,
|
|
|
+ { size: SZ_ITEM }
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ // 评估人 / 总分
|
|
|
+ parts.push(
|
|
|
+ para(
|
|
|
+ run(
|
|
|
+ `评估人:${res.assessor || ''} 初筛分:${initialScore}分 评估分:${num(
|
|
|
+ Number(assessScore) + Number(measureScore || 0)
|
|
|
+ )}分 年龄调整:${ageAdjust}分 `,
|
|
|
+ { b: true, size: SZ_INFO }
|
|
|
+ ) + run(`总分:${totalScore}分`, { b: true, font: FONT_HEI, size: SZ_INFO }),
|
|
|
+ { before: 30 }
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ // 评估结果勾选
|
|
|
+ parts.push(
|
|
|
+ para(
|
|
|
+ run('评估结果:', { b: true, size: SZ_INFO }) +
|
|
|
+ RESULT_OPTIONS.map((o) => run(`${box(riskLevel === o.level)}${o.label} `, { size: SZ_INFO })).join(''),
|
|
|
+ { before: 20 }
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ // 结果评定(最小字号,保证整页放得下)
|
|
|
+ parts.push(textPara('结果评定:', { b: true, font: FONT_HEI, size: SZ_RULE, before: 30 }))
|
|
|
+ RESULT_RULES.forEach((r) => parts.push(textPara(r, { size: SZ_RULE })))
|
|
|
+
|
|
|
+ return parts.join('')
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------- docx 打包 ----------------
|
|
|
+
|
|
|
+const CONTENT_TYPES = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
|
+<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
|
+<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
|
+<Default Extension="xml" ContentType="application/xml"/>
|
|
|
+<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
|
|
+<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
|
|
|
+</Types>`
|
|
|
+
|
|
|
+const ROOT_RELS = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
|
+<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
|
+<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
|
|
|
+</Relationships>`
|
|
|
+
|
|
|
+const DOC_RELS = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
|
+<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
|
+<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
|
|
+</Relationships>`
|
|
|
+
|
|
|
+const STYLES = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
|
+<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
|
+<w:docDefaults>
|
|
|
+<w:rPrDefault><w:rPr><w:rFonts w:ascii="宋体" w:hAnsi="宋体" w:eastAsia="宋体" w:cs="宋体"/><w:sz w:val="22"/><w:szCs w:val="22"/></w:rPr></w:rPrDefault>
|
|
|
+<w:pPrDefault><w:pPr><w:spacing w:before="0" w:after="0" w:line="${LINE_EXACT}" w:lineRule="exact"/></w:pPr></w:pPrDefault>
|
|
|
+</w:docDefaults>
|
|
|
+<w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/><w:qFormat/></w:style>
|
|
|
+</w:styles>`
|
|
|
+
|
|
|
+/** 分页符段落(除第一组外,每组长者之前插入——每人一张 A4) */
|
|
|
+const PAGE_BREAK = '<w:p><w:pPr><w:spacing w:before="0" w:after="0" w:line="160" w:lineRule="exact"/></w:pPr><w:r><w:br w:type="page"/></w:r></w:p>'
|
|
|
+
|
|
|
+const buildDocumentXml = (records: NutritionExportRecord[]): string => {
|
|
|
+ const body =
|
|
|
+ records.map((r, i) => (i > 0 ? PAGE_BREAK : '') + renderElderBlock(r)).join('') +
|
|
|
+ // A4 竖版;页边距 10mm(约567 twips),配合固定行距把内容控制在一页内
|
|
|
+ `<w:sectPr><w:pgSz w:w="11906" w:h="16838"/>` +
|
|
|
+ `<w:pgMar w:top="567" w:right="567" w:bottom="567" w:left="567" w:header="0" w:footer="0" w:gutter="0"/>` +
|
|
|
+ `</w:sectPr>`
|
|
|
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
|
+<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
|
+<w:body>${body}</w:body>
|
|
|
+</w:document>`
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 导出多组长者营养评估数据到一份 .docx(每组一页 A4)。
|
|
|
+ */
|
|
|
+export const exportNutritionDocx = async (records: NutritionExportRecord[]): Promise<void> => {
|
|
|
+ if (!records.length) throw new Error('没有可导出的评估数据')
|
|
|
+
|
|
|
+ const zip = new JSZip()
|
|
|
+ zip.file('[Content_Types].xml', CONTENT_TYPES)
|
|
|
+ zip.folder('_rels')!.file('.rels', ROOT_RELS)
|
|
|
+ const word = zip.folder('word')!
|
|
|
+ word.file('document.xml', buildDocumentXml(records))
|
|
|
+ word.file('styles.xml', STYLES)
|
|
|
+ word.folder('_rels')!.file('document.xml.rels', DOC_RELS)
|
|
|
+
|
|
|
+ const blob = await zip.generateAsync({
|
|
|
+ type: 'blob',
|
|
|
+ mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
|
+ })
|
|
|
+
|
|
|
+ const now = new Date()
|
|
|
+ const pad = (n: number) => String(n).padStart(2, '0')
|
|
|
+ const dateStr = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
|
|
|
+ saveAs(blob, `老年人营养评估表_${dateStr}.docx`)
|
|
|
+}
|