vue2-client 1.20.34 → 1.20.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue2-client",
3
- "version": "1.20.34",
3
+ "version": "1.20.35",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "serve": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint",
@@ -13,6 +13,14 @@
13
13
  <button @click="loadAndExtract">加载内容并提取</button>
14
14
  <button @click="getSelected">获取选中数据</button>
15
15
  <button @click="clearSections">清空</button>
16
+ <button @click="testExtractByKeywords">测试 extractByKeywords</button>
17
+ </div>
18
+
19
+ <div v-if="keywordResult" class="keyword-result">
20
+ <h3>extractByKeywords 结果:</h3>
21
+ <div v-for="(item, idx) in keywordResult" :key="idx" class="result-item">
22
+ <strong>{{ Object.keys(item)[0] }}:</strong>{{ Object.values(item)[0] }}
23
+ </div>
16
24
  </div>
17
25
  </div>
18
26
  </template>
@@ -27,7 +35,8 @@ export default {
27
35
  data () {
28
36
  return {
29
37
  selectedItems: [],
30
- sampleMarkdown: markdownContent
38
+ sampleMarkdown: markdownContent,
39
+ keywordResult: null
31
40
  }
32
41
  },
33
42
  methods: {
@@ -51,6 +60,15 @@ export default {
51
60
  },
52
61
  clearSections () {
53
62
  this.$refs.extractor.clearRenderedSections()
63
+ },
64
+ testExtractByKeywords () {
65
+ const result = this.$refs.extractor.extractByKeywords(
66
+ this.sampleMarkdown,
67
+ ['主诉', '现病史', '既往史', '体格检查', '专科检查', '辅助检查']
68
+ )
69
+ this.keywordResult = result
70
+ // eslint-disable-next-line no-console
71
+ console.log('extractByKeywords result:', result)
54
72
  }
55
73
  }
56
74
  }
@@ -75,4 +93,27 @@ export default {
75
93
  }
76
94
  }
77
95
  }
96
+
97
+ .keyword-result {
98
+ margin-top: 16px;
99
+ padding: 12px;
100
+ background: #f5f5f5;
101
+ border-radius: 4px;
102
+
103
+ h3 {
104
+ margin: 0 0 12px 0;
105
+ font-size: 14px;
106
+ }
107
+
108
+ .result-item {
109
+ margin-bottom: 8px;
110
+ font-size: 13px;
111
+ line-height: 1.5;
112
+ word-break: break-all;
113
+
114
+ strong {
115
+ color: #1890ff;
116
+ }
117
+ }
118
+ }
78
119
  </style>
@@ -49,7 +49,8 @@ import {
49
49
  extractSectionsFromMarkdown,
50
50
  flattenSectionsToItemsArray,
51
51
  flattenSectionsToItemsByKey,
52
- convertToFormat
52
+ convertToFormat,
53
+ extractByKeywords
53
54
  } from './markdownSectionExtractor'
54
55
 
55
56
  export default {
@@ -224,6 +225,7 @@ export default {
224
225
  return []
225
226
  }
226
227
  const sections = extractSectionsFromMarkdown(text, config)
228
+ console.log('[extractAs] sections:', JSON.stringify(sections, null, 2))
227
229
  if (parseType === 'flat') {
228
230
  return flattenSectionsToItemsArray(sections)
229
231
  }
@@ -236,6 +238,7 @@ export default {
236
238
  }))
237
239
  : []
238
240
  }))
241
+ console.log('[extractAs] result:', JSON.stringify(result, null, 2))
239
242
  // 创建新数组引用以确保 Vue 响应式更新
240
243
  const newSections = [...this.renderedSections, ...result]
241
244
  this.renderedSections = newSections
@@ -304,6 +307,39 @@ export default {
304
307
  this.$nextTick(() => {
305
308
  this.$emit('sections-rendered', this.renderedSections)
306
309
  })
310
+ },
311
+ /**
312
+ * 根据关键字提取 markdown 中各区块的内容
313
+ *
314
+ * 用途:从一份 markdown 病历中按「主诉、现病史、既往史」等标题快速提取对应的区块内容。
315
+ * 返回格式为数组,每个元素是一个以关键字为 key 的对象。
316
+ *
317
+ * 规则:
318
+ * - 每个关键字会找到对应标题(默认 2 级标题即 ## 开头),提取从该标题到下一个同级或更高等级标题之前的内容
319
+ * - 若文档中没有匹配的关键字,则内容为空字符串
320
+ * - 只返回有匹配的关键字,未匹配的不返回
321
+ *
322
+ * @param {string} markdownText - markdown 格式的字符串
323
+ * @param {string[]} keywords - 关键字数组,如 ['主诉', '现病史', '既往史']
324
+ * @param {Object} [options] - 可选配置
325
+ * @param {number} [options.headingLevel=2] - 标题级别,默认 2(对应 ##),可设为 3(对应 ###)等
326
+ * @param {boolean} [options.trimContent=true] - 是否去除内容首尾空白,默认为是
327
+ * @param {boolean} [options.stripMarkdown=true] - 是否去除 markdown 格式符号(如 **、-、引用等),默认为是
328
+ * @returns {Array<Object>} - 数组,每个元素以关键字为 key,如 [{ "主诉": "反复左额部..." }, { "现病史": "患者女性..." }]
329
+ *
330
+ * @example
331
+ * const result = this.extractByKeywords(markdownString, ['主诉', '现病史', '既往史'])
332
+ * // result = [
333
+ * // { "主诉": "反复左额部搏动性疼痛2年余,加重伴呕吐1天。" },
334
+ * // { "现病史": "患者女性,2年前无明显诱因出现左侧额部头痛..." }
335
+ * // ]
336
+ */
337
+ extractByKeywords (markdownText, keywords, options) {
338
+ if (typeof markdownText !== 'string') {
339
+ console.warn('[XMarkdownSectionExtractor] extractByKeywords requires a string as first argument')
340
+ return []
341
+ }
342
+ return extractByKeywords(markdownText, keywords, options)
307
343
  }
308
344
  }
309
345
  }
@@ -1,72 +1,53 @@
1
- ### 还需提问
2
-
3
- **1. 头痛是否与月经周期有关?**
4
-
5
- - 是,每次多在月经前发作
6
- - 否,与月经无关
7
- - 不确定,未特别注意
8
-
9
- **2. 是否存在视力变化,如短暂性视力丧失或视野缺损?**
10
-
11
- - 有,曾出现短暂视物模糊或黑蒙
12
- - 无,视力一直稳定
13
- - 偶尔有,但很快恢复
14
-
15
- **3. 是否有近期体重显著增加或减少?**
16
-
17
- - 体重明显增加(如近几个月增重>5公斤)
18
- - 体重无明显变化
19
- - 体重下降
20
-
21
- ---
22
-
23
- ### 疾病诊断
24
-
25
- 根据现病史及体格检查,该患者存在以下"红旗征":
26
-
27
- - **视乳头水肿**(强烈提示颅内压增高)
28
- - **平躺时头痛加重、站立减轻**(提示体位性颅内压变化)
29
- - **咳嗽时头痛加重**(Valsalva动作诱发)
30
- - **新发全头胀痛伴呕吐、视物模糊**,与既往偏头痛模式不同
31
-
32
- 结合**双侧视乳头水肿**及**颈强3指**,提示存在**慢性颅内压增高**,且可能合并脑膜刺激征。尽管克氏征和布氏征阴性,但颈强仍需警惕。
33
-
34
- 此外,患者为育龄女性,**近期停经**,需考虑**特发性颅内高压(IIH)**的可能。IIH常见于育龄期肥胖女性,典型表现为头痛、视乳头水肿、视力障碍及搏动性耳鸣,脑脊液压力升高但成分正常。
35
-
36
- 然而,**平躺加重、站立减轻**的体位性头痛更常见于**低颅压**,但低颅压通常不伴视乳头水肿。因此,此体位特点与典型IIH不符,反而提示**颅内压显著增高导致脑组织移位或脑室系统受压**,需进一步评估。
37
-
38
- 综合考虑,**最可能的诊断为特发性颅内高压(IIH)**,但需排除其他继发性颅内压增高病因,如**静脉窦血栓、脑肿瘤、感染等**。
39
-
40
- ---
41
-
42
- ### 下一步建议
43
-
44
- 1. **立即进行神经影像学检查**:
45
- - **首选头颅MRI+MR静脉成像(MRV)**,以评估是否存在**静脉窦血栓**、占位性病变或脑积水等结构性病因。
46
- - 若无法立即行MRI,可先做头颅CT平扫排除出血或占位。
47
-
48
- 2. **评估颅内压的金标准是腰椎穿刺**:
49
- - 在影像学排除占位后,进行**腰椎穿刺测量脑脊液(CSF)开放压**。
50
- - **CSF开放压 >250 mmH₂O**(成人)支持IIH诊断 ^[1]^。
51
- - 同时需**分析CSF成分**,以排除感染、炎症或肿瘤性脑膜炎 ^[1]^。
52
-
53
- 3. **眼科评估**:
54
- - 立即行**视野检查和视神经功能评估**,因IIH可导致永久性视力损害。
55
-
56
- 4. **诊断标准参考ICHD-3**:
57
- - 若符合**颅内压升高、正常脑脊液成分、无占位或静脉窦血栓等病因**,可诊断为**特发性颅内高压(IIH)**。
58
-
59
- 5. **治疗建议**:
60
- - **一线药物治疗**:**乙酰唑胺**(通过减少脑脊液生成降低颅压),起始剂量通常为250 mg 每日2-3次,根据耐受性调整。
61
- - **体重管理**:若存在超重或肥胖,**减重5%-10%**可显著改善症状 ^[2]^。
62
- - **严重或进行性视力损害**:需考虑**视神经鞘减压术**或**脑脊液分流术**。
63
- - **静脉窦血栓**:若MRV提示血栓,需抗凝治疗。
64
-
65
- 6. **监测与随访**:
66
- - 定期监测**视力、视野及颅内压变化**,避免不可逆视力丧失。
67
-
68
- **关键点**:
69
-
70
- - **视乳头水肿是IIH的典型体征**,但需排除其他继发性病因。
71
- - **CSF开放压是诊断IIH的核心指标** ^[1]^。
72
- - **及时干预可防止永久性视力损害**。
1
+ # 病例分析
2
+
3
+ ## 主诉
4
+
5
+ 反复左额部搏动性疼痛2年余,加重伴呕吐1天。
6
+
7
+ ## 现病史
8
+
9
+ 患者女性,2年前无明显诱因出现左侧额部头痛,呈阵发性搏动性疼痛,每次持续12小时至3天,平均每月发作1-2次,疼痛程度为VAS 8分,发作时常伴畏光、畏声,严重时伴有恶心、呕吐。头痛在日常活动后加重,吹风、受凉及工作压力大时易诱发。
10
+
11
+ 1天前再次发作头痛,性质与以往不同,转为**全头胀痛**,疼痛程度加重至VAS 10分,**平躺时加重,站立时减轻**,且在咳嗽时明显加重。自服布洛芬2粒后症状无缓解,且持续加重,伴恶心、呕吐及视物模糊。
12
+
13
+ 值得注意的是,此次头痛具有**体位相关性**,**站立时减轻、平卧时加重**,提示可能与颅内压变化相关。此外,咳嗽时头痛加重,提示颅内压波动或脑脊液动力学异常。近期存在**用力排便**等增加腹压的行为史,可能为脑脊液漏的诱因之一。同时,患者自述**近期体重明显增加**,需考虑是否存在内分泌或代谢异常,如甲状腺功能减退、库欣综合征等,但尚无其他典型症状支持。
14
+
15
+ ## 既往史
16
+
17
+ 否认肿瘤史、高血压、脑动脉瘤、脑动静脉畸形;无家族性脑血管病史;否认近期头面部感染或头部创伤;2个月前停经,末次月经为2025年1月3日;否认药物过敏及手术史。
18
+
19
+ ## 体格检查
20
+
21
+ - 血压:133/75 mmHg
22
+ - 脉搏:82次/分
23
+ - 体温:36.4°C
24
+ - 神志:清楚,焦虑面容
25
+
26
+ 专科检查:
27
+
28
+ - **双侧视乳头水肿**,眼球活动正常,其余脑神经查体未见明显异常
29
+ - **颈强3指**,克氏征和布氏征阴性
30
+ - 四肢运动、感觉检查未见异常
31
+
32
+ ## 辅助检查
33
+
34
+ 目前尚未进行影像学或脑脊液检查。根据临床表现,需高度怀疑**自发性低颅压综合征(SIH)**,尤其是其典型特征如**体位性头痛(站立减轻、平卧加重)**、**咳嗽加重**、**视乳头水肿**及**颈部僵硬**等。
35
+
36
+ ### 脑脊液检查要点
37
+
38
+ 文献显示,**并非所有SIH患者脑脊液压力均低于正常**。一项118例的回顾性研究显示,**79.6%的患者脑脊液压力在60-250 mmH₂O之间**,甚至**1.9%的患者压力高于250 mmH₂O** ^[7]^。此外,**病程越短,脑脊液压力越低,随病程延长压力可逐渐回升至正常范围** ^[7]^。
39
+
40
+ **脑脊液蛋白水平在57.5%的患者中超过45 mg/dl**,**红细胞计数在50.6%的患者中超过8.0×10⁶/L**,可能与硬膜外静脉丛扩张损伤有关 ^[7]^。
41
+
42
+ ### 进一步检查建议
43
+
44
+ 应尽快完善:
45
+
46
+ 1. **头颅MRI增强扫描** — 评估是否存在硬脑膜强化、硬膜下积液、静脉窦增宽等SIH典型表现
47
+ 2. **脊髓影像学检查**(如脊髓水成像或CT脊髓造影) — 寻找脑脊液漏点
48
+
49
+ 若确诊,**靶向硬膜外血贴(EBP)治疗**可能为有效干预手段。
50
+
51
+ ---
52
+
53
+ 建议进一步查阅自发性低颅压综合征的诊断标准及脑脊液动力学异常在体重增加背景下的潜在内分泌关联的最新临床指南。
@@ -129,9 +129,13 @@ function buildHierarchicalItems (sectionLines) {
129
129
  if (!Array.isArray(sectionLines) || sectionLines.length === 0) return []
130
130
 
131
131
  const result = []
132
- // 匹配 "1. xxx" 或 "**1. xxx**" 格式的数字标题
133
- const numberLineRegex = /^\s*\*{0,2}(\d+)\.\s+(.+?)\*{0,2}\s*$/
134
- const bulletRegex = /^\s*[-*+]\s+(.*)$/
132
+ // 匹配数字标题:"1. xxx" 或 "**1. xxx**"(行首必须是数字或 **)
133
+ // 使用捕获组:组1=数字(普通), 2=内容(普通),组3=数字(加粗), 组4=内容(加粗)
134
+ const numberLineRegex = /^(?:(\d+)\.\s+(.+)|\*\*(\d+)\.\s+(.+)\*\*)$/
135
+ // 匹配 bullet 标题:"- **xxx**"(行首是 -)
136
+ const bulletTitleRegex = /^-\s+\*\*(.+)\*\*$/
137
+ // 匹配 bullet 内容:"- xxx"(行首是 -,后面不是 **)
138
+ const bulletRegex = /^-\s+([^*].*)$/
135
139
 
136
140
  let currentTitle = null
137
141
  let currentBullets = []
@@ -156,16 +160,38 @@ function buildHierarchicalItems (sectionLines) {
156
160
  // 跳过空行
157
161
  if (!trimmed.trim()) continue
158
162
 
163
+ // 调试日志
164
+ // console.log('Processing line:', JSON.stringify(trimmed))
165
+
166
+ // 匹配数字标题 "1. xxx" 或 "**1. xxx**"
159
167
  const numMatch = numberLineRegex.exec(trimmed)
160
168
  if (numMatch) {
169
+ // 组1=数字(普通), 组2=内容(普通),组3=数字(加粗), 组4=内容(加粗)
170
+ flushCurrent()
171
+ currentTitle = numMatch[2] || numMatch[4] || ''
172
+ continue
173
+ }
174
+
175
+ // 匹配 bullet 标题 "- **标题**"
176
+ const bulletTitleMatch = bulletTitleRegex.exec(trimmed)
177
+ if (bulletTitleMatch) {
178
+ // console.log('Matched bullet title:', bulletTitleMatch[1])
161
179
  flushCurrent()
162
- currentTitle = numMatch[2] || ''
180
+ currentTitle = bulletTitleMatch[1] || ''
163
181
  continue
164
182
  }
165
183
 
184
+ // 匹配 bullet 内容 "- xxx"
166
185
  const bulletMatch = bulletRegex.exec(trimmed)
167
186
  if (bulletMatch) {
187
+ // console.log('Matched bullet content:', bulletMatch[1])
168
188
  currentBullets.push(bulletMatch[1] || '')
189
+ continue
190
+ }
191
+
192
+ // 处理缩进的普通文本(无前缀),作为内容
193
+ if (/^\s{2,}/.test(trimmed) && currentTitle !== null) {
194
+ currentBullets.push(trimmed)
169
195
  }
170
196
  }
171
197
 
@@ -197,73 +223,72 @@ function buildStructuredItems (sectionLines) {
197
223
  if (!Array.isArray(sectionLines) || sectionLines.length === 0) return []
198
224
 
199
225
  const items = []
200
- // 匹配 "1. xxx" 或 "**1. xxx**" 格式的数字标题
201
- const numberLineRegex = /^\s*\*{0,2}(\d+)\.\s+(.+?)\*{0,2}\s*$/
202
- const bulletRegex = /^\s*[-*+]\s+(.*)$/
203
-
204
- // 先收集所有数字开头的行索引
205
- const numberIndexes = []
226
+ // 匹配数字标题:"1. xxx" 或 "**1. xxx**"
227
+ const numberLineRegex = /^(?:(\d+)\.\s+(.+)|\*\*(\d+)\.\s+(.+)\*\*)$/
228
+ // 匹配 bullet 标题:"- **xxx**"
229
+ const bulletTitleRegex = /^-\s+\*\*(.+)\*\*$/
230
+ // 匹配 bullet 内容:"- xxx"(不包含 ** 的)
231
+ const bulletRegex = /^-\s+([^*].*)$/
232
+
233
+ // 先收集所有标题行(数字或 bullet)
234
+ const titleIndexes = []
206
235
  sectionLines.forEach((line, idx) => {
207
236
  if (numberLineRegex.test(line || '')) {
208
- numberIndexes.push(idx)
237
+ titleIndexes.push({ idx, type: 'number' })
238
+ } else if (bulletTitleRegex.test(line || '')) {
239
+ titleIndexes.push({ idx, type: 'bullet' })
209
240
  }
210
241
  })
211
242
 
212
- if (numberIndexes.length === 0) {
213
- // 没有数字列表时,整体作为一个 item 返回
243
+ if (titleIndexes.length === 0) {
214
244
  const joined = sectionLines.join(' ').trim()
215
245
  if (!joined) return []
216
246
  const plain = stripMarkdownEmphasis(joined)
217
- return [{
218
- id: 1,
219
- name: plain
220
- }]
247
+ return [{ id: 1, name: plain }]
221
248
  }
222
249
 
223
- numberIndexes.forEach((startIdx, idx) => {
250
+ titleIndexes.forEach((titleInfo, idx) => {
251
+ const startIdx = titleInfo.idx
224
252
  const line = sectionLines[startIdx] || ''
225
- const m = numberLineRegex.exec(line)
226
- if (!m) return
227
253
 
228
- const rawAfterNumber = m[2] || ''
229
- const isFirstNumberItem = idx === 0
230
-
231
- let candidateText = rawAfterNumber
254
+ let titleText = ''
255
+ if (titleInfo.type === 'number') {
256
+ const m = numberLineRegex.exec(line)
257
+ // numMatch[2] "1. xxx" 的内容,numMatch[4] 是 "**1. xxx**" 的内容
258
+ titleText = m ? (m[2] || m[4] || '') : ''
259
+ } else {
260
+ const m = bulletTitleRegex.exec(line)
261
+ titleText = m ? (m[1] || '') : ''
262
+ }
232
263
 
233
- // 尝试向后看,收集紧跟的 bullet 行
234
264
  const bullets = []
235
- for (let i = startIdx + 1; i < sectionLines.length; i++) {
265
+ const nextIdx = titleIndexes[idx + 1] ? titleIndexes[idx + 1].idx : sectionLines.length
266
+
267
+ for (let i = startIdx + 1; i < nextIdx; i++) {
236
268
  const l = sectionLines[i] || ''
237
269
  if (!l.trim()) continue
238
- // 遇到下一个数字条目则终止
239
- if (numberLineRegex.test(l)) break
240
270
  const bm = bulletRegex.exec(l)
241
271
  if (bm) {
242
272
  bullets.push(bm[1] || '')
243
- } else {
244
- // 非 bullet 行(比如解释性文本),这里先忽略,不打断
273
+ } else if (/^\s{2,}/.test(l)) {
274
+ bullets.push(l.trim())
245
275
  }
246
276
  }
247
277
 
248
- if (isFirstNumberItem && bullets.length > 0) {
249
- // 第一个数字条目:优先用第一个 bullet 的内容
278
+ let candidateText = titleText
279
+ if (bullets.length > 0) {
250
280
  candidateText = bullets[0]
251
281
  }
252
282
 
253
- // 截取中文全角冒号前面的部分
254
283
  const colonIndex = candidateText.indexOf(':')
255
284
  if (colonIndex > 0) {
256
285
  candidateText = candidateText.slice(0, colonIndex)
257
286
  }
258
287
 
259
288
  const name = stripMarkdownEmphasis(candidateText).trim()
260
-
261
289
  if (!name) return
262
290
 
263
- items.push({
264
- id: items.length + 1,
265
- name
266
- })
291
+ items.push({ id: items.length + 1, name })
267
292
  })
268
293
 
269
294
  return items
@@ -532,3 +557,95 @@ export function convertToXListDefault (sections, options = {}) {
532
557
  export function convertToXListCard (sections, options = {}) {
533
558
  return convertToFormat(sections, OutputFormat.XLIST_CARD, options)
534
559
  }
560
+
561
+ /**
562
+ * 去除 markdown 格式符号,保留纯文本
563
+ * - 去掉 ** 和 * 强调标记
564
+ * - 去掉 - bullet 列表标记(行首的 "- ")
565
+ * - 去掉 ^[数字]^ 引用标记
566
+ * - 去掉 [文字](url) 链接格式,保留文字
567
+ *
568
+ * @param {string} text
569
+ * @returns {string}
570
+ */
571
+ function stripMarkdownFormat (text) {
572
+ if (!text || typeof text !== 'string') return ''
573
+ return text
574
+ // 去掉 ^[数字]^ 引用格式
575
+ .replace(/\^\[(\d+)\]\^/g, '')
576
+ // 去掉加粗 **xxx**
577
+ .replace(/\*\*(.+?)\*\*/g, '$1')
578
+ // 去掉斜体 *xxx*
579
+ .replace(/\*(.+?)\*/g, '$1')
580
+ // 去掉行首的 "- "(保留后面的内容)
581
+ .replace(/^- /gm, '')
582
+ // 去掉 [文字](url) 链接格式,保留文字
583
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
584
+ // 清理多余的空行(保留最多一个空行)
585
+ .replace(/\n{3,}/g, '\n\n')
586
+ }
587
+
588
+ /**
589
+ * 根据关键字提取 markdown 中各区块的内容
590
+ *
591
+ * 用途:从一份 markdown 病历中按「主诉、现病史、既往史」等标题快速提取对应的区块内容。
592
+ * 返回格式为数组,每个元素是一个以关键字为 key 的对象。
593
+ *
594
+ * 规则:
595
+ * - 每个关键字会找到对应标题(默认 2 级标题即 ## 开头),提取从该标题到下一个同级或更高等级标题之前的内容
596
+ * - 若文档中没有匹配的关键字,则该关键字对应的内容为空字符串
597
+ * - 只返回有匹配的关键字,未匹配的不返回
598
+ *
599
+ * @param {string} markdownText - markdown 格式的字符串
600
+ * @param {string[]} keywords - 关键字数组,如 ['主诉', '现病史', '既往史']
601
+ * @param {Object} [options] - 可选配置
602
+ * @param {number} [options.headingLevel=2] - 标题级别,默认 2(对应 ##),可设为 3(对应 ###)等
603
+ * @param {boolean} [options.trimContent=true] - 是否去除内容首尾空白,默认为是
604
+ * @param {boolean} [options.stripMarkdown=true] - 是否去除 markdown 格式符号(如 **、-、引用等),默认为是
605
+ * @returns {Array<Object>} - 数组,每个元素以关键字为 key,如 [{ "主诉": "反复左额部..." }, { "现病史": "患者女性..." }]
606
+ *
607
+ * @example
608
+ * const md = `## 主诉\n反复左额部搏动性疼痛2年余,加重伴呕吐1天。\n\n## 现病史\n患者女性,2年前无明显诱因出现左侧额部头痛...`
609
+ * const result = extractByKeywords(md, ['主诉', '现病史', '既往史'])
610
+ * // result = [
611
+ * // { "主诉": "反复左额部搏动性疼痛2年余,加重伴呕吐1天。" },
612
+ * // { "现病史": "患者女性,2年前无明显诱因出现左侧额部头痛..." }
613
+ * // ]
614
+ */
615
+ export function extractByKeywords (markdownText, keywords, options = {}) {
616
+ if (!markdownText || typeof markdownText !== 'string') return []
617
+ if (!Array.isArray(keywords) || keywords.length === 0) return []
618
+
619
+ const {
620
+ headingLevel = 2,
621
+ trimContent = true,
622
+ stripMarkdown = true
623
+ } = options
624
+
625
+ const lines = markdownText.split(/\r?\n/)
626
+ const results = []
627
+
628
+ keywords.forEach(keyword => {
629
+ const cfg = {
630
+ key: `kw_${keyword}`,
631
+ label: String(keyword).trim(),
632
+ headingLevel
633
+ }
634
+ const headingIndex = findHeadingIndex(lines, cfg)
635
+
636
+ let content = ''
637
+ if (headingIndex !== -1) {
638
+ const range = findSectionRange(lines, headingIndex)
639
+ if (range.start <= range.end) {
640
+ const sectionLines = lines.slice(range.start, range.end + 1)
641
+ content = sectionLines.join('\n')
642
+ if (trimContent) content = content.trim()
643
+ if (stripMarkdown) content = stripMarkdownFormat(content)
644
+ }
645
+ }
646
+
647
+ results.push({ [keyword]: content })
648
+ })
649
+
650
+ return results
651
+ }