vue2-client 1.20.27 → 1.20.29

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.27",
3
+ "version": "1.20.29",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "serve": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint",
@@ -2,132 +2,36 @@
2
2
  <div class="demo-x-markdown-section-extractor">
3
3
  <h2>Demo: XMarkdownSectionExtractor + bingli.md</h2>
4
4
 
5
- <!-- xlist-default 格式:XList 默认模式 -->
6
5
  <XMarkdownSectionExtractor
6
+ ref="extractor"
7
7
  :source="bingliMd"
8
- :sectionsConfig="sectionsConfig"
9
- :showUploadButton="true"
10
- @upload="onUploadMarkdown"
11
- outputFormat="xlist-default"
12
- v-slot="{ formattedData, loading, error }"
13
- >
14
- <div class="result-panel">
15
- <div v-if="loading" class="state-text">正在抽取中...</div>
16
- <div v-else-if="error" class="state-text error">
17
- 抽取失败:{{ error && (error.message || error) }}
18
- </div>
19
- <div v-else>
20
- <h3>XList 默认模式 (xlist-default)</h3>
21
- <p class="format-desc">使用 initialData 传入 XList 组件</p>
22
- <XList :initialData="formattedData" />
23
- </div>
24
- </div>
25
- </XMarkdownSectionExtractor>
26
-
27
- <!-- xlist-title-content 格式:标题-内容模式 -->
28
- <XMarkdownSectionExtractor
29
- :source="bingliMd"
30
- :sectionsConfig="sectionsConfig"
31
8
  :showUploadButton="false"
32
- outputFormat="xlist-title-content"
33
- v-slot="{ formattedData, loading, error }"
34
- >
35
- <div class="result-panel">
36
- <div v-if="loading" class="state-text">正在抽取中...</div>
37
- <div v-else-if="error" class="state-text error">
38
- 抽取失败:{{ error && (error.message || error) }}
39
- </div>
40
- <div v-else>
41
- <h3>XList 标题-内容模式 (xlist-title-content)</h3>
42
- <p class="format-desc">使用 XTitle + XList 组合渲染</p>
43
- <div v-for="(section, sIdx) in formattedData" :key="sIdx" class="section-block">
44
- <!-- 区块标题 -->
45
- <XTitle :title="section.sectionTitle" />
46
- <!-- 每个建议项 -->
47
- <div v-for="(item, iIdx) in section.items" :key="iIdx" class="item-block">
48
- <XTitle :title="item.title" littlefont />
49
- <XList v-if="item.content && item.content.length" :initialData="item.content.map((c, ci) => ({ number: ci + 1, name: c }))" />
50
- </div>
51
- </div>
52
- </div>
53
- </div>
54
- </XMarkdownSectionExtractor>
9
+ @extracted="onExtracted"
10
+ />
55
11
  </div>
56
12
  </template>
57
13
 
58
14
  <script>
59
15
  import XMarkdownSectionExtractor from './XMarkdownSectionExtractor.vue'
60
- import XList from '@vue2-client/base-client/components/his/XList/XList.vue'
61
- import XTitle from '@vue2-client/base-client/components/his/XTitle/XTitle.vue'
62
16
  import bingliMd from './bingli.md'
63
17
 
64
18
  export default {
65
19
  name: 'DemoXMarkdownSectionExtractor',
66
- components: { XMarkdownSectionExtractor, XList, XTitle },
20
+ components: { XMarkdownSectionExtractor },
67
21
  data () {
68
22
  return {
69
- bingliMd,
70
- sectionsConfig: [
71
- {
72
- key: 'nextStepSuggestion',
73
- label: '下一步建议',
74
- headingLevel: 3
75
- }
76
- ]
23
+ bingliMd
77
24
  }
78
25
  },
26
+ mounted () {
27
+ const keywords = ['下一步建议', '诊断依据']
28
+ this.$refs.extractor.extractAs(keywords, 'title-content', true)
29
+ },
79
30
  methods: {
80
- onUploadMarkdown (file) {
81
- this.bingliMd = file
31
+ onExtracted (sections) {
32
+ // eslint-disable-next-line no-console
33
+ console.log('extracted sections:', sections)
82
34
  }
83
35
  }
84
36
  }
85
37
  </script>
86
-
87
- <style lang="less" scoped>
88
- .demo-x-markdown-section-extractor {
89
- padding: 16px;
90
- font-size: 14px;
91
- line-height: 1.6;
92
- }
93
-
94
- .result-panel {
95
- margin-top: 12px;
96
- padding: 12px;
97
- border-radius: 4px;
98
- border: 1px solid #f0f0f0;
99
- background-color: #fafafa;
100
- }
101
-
102
- .state-text {
103
- color: rgba(0, 0, 0, 0.45);
104
- }
105
-
106
- .state-text.error {
107
- color: #ff4d4f;
108
- }
109
-
110
- .format-desc {
111
- color: rgba(0, 0, 0, 0.65);
112
- font-size: 13px;
113
- margin-bottom: 8px;
114
- }
115
-
116
- .section-block {
117
- margin-bottom: 16px;
118
- padding-bottom: 16px;
119
- border-bottom: 1px dashed #e8e8e8;
120
-
121
- &:last-child {
122
- border-bottom: none;
123
- margin-bottom: 0;
124
- }
125
- }
126
-
127
- .item-block {
128
- margin: 8px 0 8px 16px;
129
- padding-left: 12px;
130
- border-left: 2px solid #1890ff;
131
- }
132
- </style>
133
-
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div class="x-markdown-section-extractor">
3
- <!-- 左上角上传按钮(可通过 showUploadButton 关闭) -->
3
+ <!-- 左上角上传按钮 -->
4
4
  <div v-if="showUploadButton" class="extractor-header">
5
5
  <input
6
6
  ref="fileInput"
@@ -9,13 +9,23 @@
9
9
  class="file-input"
10
10
  @change="handleFileUpload"
11
11
  >
12
- <button type="button" class="upload-btn" @click.prevent="triggerFileSelect">
13
- 上传 Markdown
14
- </button>
15
12
  </div>
16
13
 
17
- <!-- 这个组件的主要作用是:读取 markdown + 抽取 JSON,然后通过事件抛出给外层 -->
18
- <!-- 默认不渲染任何可见内容,只在需要调试时可以打开简单输出 -->
14
+ <!-- 渲染 extractAs 返回的 sections -->
15
+ <div v-if="renderedSections.length" class="rendered-sections">
16
+ <div v-for="(sec, idx) in renderedSections" :key="idx" class="section-block">
17
+ <div class="section-title">{{ sec.sectionTitle }}</div>
18
+ <div v-for="(item, iIdx) in sec.items" :key="iIdx" class="item-block">
19
+ <div class="item-title">{{ item.title }}</div>
20
+ <div v-if="item.content.length" class="item-list">
21
+ <div v-for="(c, cIdx) in item.content" :key="cIdx" class="list-item">
22
+ <span class="item-number">{{ cIdx + 1 }}.</span> {{ c }}
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </div>
28
+
19
29
  <slot
20
30
  :sections="sections"
21
31
  :itemsArray="itemsArray"
@@ -23,9 +33,7 @@
23
33
  :formattedData="computedFormattedData"
24
34
  :loading="loading"
25
35
  :error="error"
26
- >
27
- <!-- 默认插槽内容:什么都不展示 -->
28
- </slot>
36
+ />
29
37
  </div>
30
38
  </template>
31
39
 
@@ -41,43 +49,18 @@ import {
41
49
  export default {
42
50
  name: 'XMarkdownSectionExtractor',
43
51
  props: {
44
- /**
45
- * markdown 源,支持:
46
- * - 直接传 markdown 字符串
47
- * - import md from './a.md'
48
- * - File / Blob
49
- * - URL/相对路径(.md 结尾)
50
- */
51
52
  source: {
52
53
  type: [String, Object],
53
54
  required: true
54
55
  },
55
- /**
56
- * 区块配置
57
- * 示例:
58
- * [
59
- * { key: 'nextStepSuggestion', label: '下一步建议', headingLevel: 3 }
60
- * ]
61
- */
62
56
  sectionsConfig: {
63
57
  type: Array,
64
58
  default: () => []
65
59
  },
66
- /**
67
- * 是否在 mounted 时立即读取 + 抽取
68
- */
69
60
  autoExtract: {
70
61
  type: Boolean,
71
62
  default: true
72
63
  },
73
- /**
74
- * 输出格式配置
75
- * 可选值:
76
- * - 'default': 返回原始 sections 数组
77
- * - 'xlist-default': 返回 XList 默认模式格式 [{ number, name }, ...]
78
- * - 'xlist-card': 返回 XList 卡片模式格式二维数组
79
- * - 'xlist-title-content': 返回 XList 标题-内容模式 [{ sectionTitle, items: [{ title, content: [] }] }]
80
- */
81
64
  outputFormat: {
82
65
  type: String,
83
66
  default: 'default',
@@ -85,18 +68,10 @@ export default {
85
68
  return ['default', 'xlist-default', 'xlist-card', 'xlist-title-content'].includes(value)
86
69
  }
87
70
  },
88
- /**
89
- * 输出格式选项,用于字段映射
90
- * xlist-default 模式: { numberField: 'number', nameField: 'name' }
91
- * xlist-card 模式: { labelField: 'label', valueField: 'name' }
92
- */
93
71
  outputOptions: {
94
72
  type: Object,
95
73
  default: () => ({})
96
74
  },
97
- /**
98
- * 是否显示左上角「上传 Markdown」按钮
99
- */
100
75
  showUploadButton: {
101
76
  type: Boolean,
102
77
  default: true
@@ -110,11 +85,11 @@ export default {
110
85
  itemsArray: [],
111
86
  itemsByKey: {},
112
87
  formattedData: null,
113
- currentSource: null
88
+ currentSource: null,
89
+ renderedSections: []
114
90
  }
115
91
  },
116
92
  computed: {
117
- // 根据 outputFormat 动态计算格式化后的数据
118
93
  computedFormattedData () {
119
94
  if (!this.sections || this.sections.length === 0) {
120
95
  return this.outputFormat === 'default' ? [] : (this.outputFormat === 'xlist-card' ? [] : [])
@@ -155,44 +130,22 @@ export default {
155
130
  deep: true
156
131
  }
157
132
  },
158
- mounted () {
159
- // source 的 watch 会在 mounted 之前以 immediate 模式执行,此处无需重复调用
160
- },
161
133
  methods: {
162
- /**
163
- * 触发文件选择
164
- */
165
- triggerFileSelect () {
166
- const input = this.$refs.fileInput
167
- if (input) {
168
- input.click()
169
- }
170
- },
171
- /**
172
- * 处理文件上传
173
- */
174
134
  handleFileUpload (event) {
175
135
  const file = event.target.files && event.target.files[0]
176
136
  if (!file) return
177
137
 
178
- // 检查文件类型
179
138
  if (!file.name.endsWith('.md') && !file.name.endsWith('.markdown') && file.type !== 'text/markdown') {
180
139
  this.$emit('error', new Error('请上传 .md 或 .markdown 文件'))
181
140
  return
182
141
  }
183
142
 
184
- // 通知父组件更新 source(父组件可监听后统一更新,多个实例会一起更新)
185
143
  this.$emit('upload', file)
186
- // 本实例也更新并解析
187
144
  this.currentSource = file
188
145
  this.extract()
189
146
 
190
- // 清空 input,以便重复选择同一文件
191
147
  event.target.value = ''
192
148
  },
193
- /**
194
- * 手动触发一次读取 + 抽取
195
- */
196
149
  async extract () {
197
150
  if (!this.currentSource) {
198
151
  return
@@ -207,7 +160,6 @@ export default {
207
160
  this.itemsArray = itemsArray
208
161
  const itemsByKey = flattenSectionsToItemsByKey(sections)
209
162
  this.itemsByKey = itemsByKey
210
- // 格式化数据
211
163
  const formattedData = convertToFormat(sections, this.outputFormat, this.outputOptions)
212
164
  this.formattedData = formattedData
213
165
  this.$emit('update:sections', sections)
@@ -220,7 +172,6 @@ export default {
220
172
  this.$emit('formatted-data', formattedData)
221
173
  return sections
222
174
  } catch (e) {
223
- // eslint-disable-next-line no-console
224
175
  console.error('[XMarkdownSectionExtractor] 抽取失败:', e)
225
176
  this.error = e
226
177
  this.sections = []
@@ -232,6 +183,36 @@ export default {
232
183
  } finally {
233
184
  this.loading = false
234
185
  }
186
+ },
187
+ async extractAs (keywords = [], parseType = 'title-content', asComponent = false) {
188
+ if (!Array.isArray(keywords) || keywords.length === 0) {
189
+ return []
190
+ }
191
+ const config = keywords.map((label, idx) => ({
192
+ key: `section_${idx}`,
193
+ label,
194
+ headingLevel: 3
195
+ }))
196
+ const sections = extractSectionsFromMarkdown(
197
+ await readMarkdownAsText(this.currentSource),
198
+ config
199
+ )
200
+ if (parseType === 'flat') {
201
+ return flattenSectionsToItemsArray(sections)
202
+ }
203
+ const result = sections.map(sec => ({
204
+ sectionTitle: sec.label || '',
205
+ items: Array.isArray(sec.hierarchicalItems)
206
+ ? sec.hierarchicalItems.map(h => ({
207
+ title: h.title || '',
208
+ content: Array.isArray(h.items) ? h.items : []
209
+ }))
210
+ : []
211
+ }))
212
+ if (asComponent) {
213
+ this.renderedSections = result
214
+ }
215
+ return result
235
216
  }
236
217
  }
237
218
  }
@@ -250,23 +231,57 @@ export default {
250
231
  display: none;
251
232
  }
252
233
 
253
- .upload-btn {
254
- padding: 4px 12px;
255
- font-size: 12px;
256
- color: #fff;
257
- background-color: #1890ff;
258
- border: none;
259
- border-radius: 3px;
260
- cursor: pointer;
261
- transition: background-color 0.3s;
234
+ .rendered-sections {
235
+ padding: 16px;
236
+ background: #fff;
237
+ border-radius: 4px;
238
+ border: 1px solid #f0f0f0;
239
+ }
262
240
 
263
- &:hover {
264
- background-color: #40a9ff;
265
- }
241
+ .section-block {
242
+ margin-bottom: 20px;
243
+ padding-bottom: 16px;
244
+ border-bottom: 1px dashed #e8e8e8;
266
245
 
267
- &:active {
268
- background-color: #096dd9;
246
+ &:last-child {
247
+ border-bottom: none;
248
+ margin-bottom: 0;
269
249
  }
270
250
  }
271
- </style>
272
251
 
252
+ .section-title {
253
+ font-size: 16px;
254
+ font-weight: 600;
255
+ color: #333;
256
+ margin-bottom: 12px;
257
+ }
258
+
259
+ .item-block {
260
+ margin: 8px 0 8px 16px;
261
+ padding-left: 12px;
262
+ border-left: 2px solid #1890ff;
263
+ }
264
+
265
+ .item-title {
266
+ font-size: 14px;
267
+ font-weight: 500;
268
+ color: #555;
269
+ margin-bottom: 6px;
270
+ }
271
+
272
+ .item-list {
273
+ padding-left: 4px;
274
+ }
275
+
276
+ .list-item {
277
+ padding: 4px 0;
278
+ color: #666;
279
+ font-size: 13px;
280
+ }
281
+
282
+ .item-number {
283
+ color: #1890ff;
284
+ font-weight: 500;
285
+ margin-right: 6px;
286
+ }
287
+ </style>
@@ -1,13 +1,14 @@
1
1
  <template>
2
- <div
3
- class="drawer"
4
- :class="[{ 'drawer-collapsed': !isOpen }, wrapperClassObject()]"
5
- :style="drawerStyle">
6
2
  <div
7
- v-show="!isFrameConfig"
3
+ ref="drawerRoot"
4
+ class="drawer"
5
+ :class="[{ 'drawer-collapsed': !isOpen }, wrapperClassObject()]"
6
+ :style="drawerStyle">
7
+ <div
8
+ v-show="!isFrameConfig || showToggleInFrame"
8
9
  class="drawer-toggle"
9
10
  :class="{ 'toggle-collapsed': !isOpen }"
10
- :style="toggleStyle"
11
+ :style="toggleButtonStyle"
11
12
  @click="toggleDrawer">
12
13
  <div class="arrow">
13
14
  {{ isOpen ? '›' : '‹' }}
@@ -48,6 +49,11 @@ export default {
48
49
  type: String,
49
50
  default: 'af-his'
50
51
  },
52
+ // Frame 配置下是否依然显示收缩按钮(默认为 false,保持兼容老用法)
53
+ showToggleInFrame: {
54
+ type: Boolean,
55
+ default: false
56
+ },
51
57
  // 是否允许该组件主动调整同级布局(默认不影响其它页面)
52
58
  affectLayout: {
53
59
  type: Boolean,
@@ -86,13 +92,25 @@ export default {
86
92
  type: String,
87
93
  default: 'percent',
88
94
  validator: value => ['percent', 'px'].includes(value)
95
+ },
96
+ // 折叠时保持的行高(px),由外部传入
97
+ rowHeight: {
98
+ type: Number,
99
+ default: null
89
100
  }
90
101
  },
91
102
  data () {
92
103
  return {
93
104
  isOpen: this.queryParamsName?.endsWith('Frame') || false,
94
105
  // 定义主内容区域的最大和最小宽度百分比
95
- mainWithData: [{ max: 80, min: 50 }]
106
+ mainWithData: [{ max: 80, min: 50 }],
107
+ rowResizeObserver: null,
108
+ drawerResizeObserver: null,
109
+ _syncDrawerMinHeightRafId: null,
110
+ // 记录所在 a-row 有史以来出现过的最大高度,只增不减,展开/收起基准统一
111
+ _maxRowHeight: null,
112
+ // 记录抽屉自身高度,用于按钮居中
113
+ _drawerHeight: null
96
114
  }
97
115
  },
98
116
  mounted () {
@@ -102,8 +120,120 @@ export default {
102
120
  if (this.isOpen) {
103
121
  this.updateCardBodyPadding()
104
122
  }
123
+ // 同步计算一次最大行高(此时 this.$el 已挂载,computed 属性立即响应,按钮 top 立即就位)
124
+ this.computeMaxRowHeight()
125
+ this.$nextTick(() => {
126
+ this.bindRowResizeObserver()
127
+ this.bindDrawerResizeObserver()
128
+ })
129
+ },
130
+ beforeDestroy () {
131
+ if (this._syncDrawerMinHeightRafId) {
132
+ cancelAnimationFrame(this._syncDrawerMinHeightRafId)
133
+ this._syncDrawerMinHeightRafId = null
134
+ }
135
+ this.unbindRowResizeObserver()
136
+ this.unbindDrawerResizeObserver()
105
137
  },
106
138
  methods: {
139
+ getSidebarParentRow () {
140
+ if (!this.$el) return null
141
+ let currentCol = this.$el.parentNode
142
+ while (
143
+ currentCol &&
144
+ (!currentCol.className ||
145
+ !currentCol.className.includes ||
146
+ !currentCol.className.includes('ant-col'))
147
+ ) {
148
+ currentCol = currentCol.parentNode
149
+ }
150
+ if (!currentCol || !currentCol.parentNode) return null
151
+ const row = currentCol.parentNode
152
+ if (!row.className || !row.className.includes('ant-row')) return null
153
+ return row
154
+ },
155
+ // 展开/收起共用同一套参照:用 CSS 变量记录行高,箭头始终相对于该值居中,
156
+ // 不走 CSS transition,彻底避免 min-height 动画化导致的"缓慢下移"问题
157
+ scheduleSyncDrawerMinHeight () {
158
+ if (this._syncDrawerMinHeightRafId) return
159
+ this._syncDrawerMinHeightRafId = requestAnimationFrame(() => {
160
+ this._syncDrawerMinHeightRafId = null
161
+ this.syncDrawerMinHeightToParentRow()
162
+ })
163
+ },
164
+ // 同步读取行高并更新 _maxRowHeight,供 mounted 在渲染前调用
165
+ computeMaxRowHeight () {
166
+ const root = this.$refs.drawerRoot
167
+ if (!root) return
168
+ const row = this.getSidebarParentRow()
169
+ if (!row) return
170
+ const raw = row.offsetHeight || row.getBoundingClientRect().height
171
+ const h = Math.round(raw)
172
+ if (h > 0 && (this._maxRowHeight === null || h > this._maxRowHeight)) {
173
+ this._maxRowHeight = h
174
+ }
175
+ },
176
+ syncDrawerMinHeightToParentRow () {
177
+ this.$nextTick(() => {
178
+ const root = this.$refs.drawerRoot
179
+ if (!root) return
180
+ const row = this.getSidebarParentRow()
181
+ if (!row) return
182
+ const raw = row.offsetHeight || row.getBoundingClientRect().height
183
+ const h = Math.round(raw)
184
+ if (h <= 0) return
185
+ // 只增不减:记录有史以来出现过的最大行高,作为收起后的定位基准
186
+ if (this._maxRowHeight === null || h > this._maxRowHeight) {
187
+ this._maxRowHeight = h
188
+ }
189
+ if (!this.rowResizeObserver && typeof ResizeObserver !== 'undefined') {
190
+ this.rowResizeObserver = new ResizeObserver(() => {
191
+ this.scheduleSyncDrawerMinHeight()
192
+ })
193
+ this.rowResizeObserver.observe(row)
194
+ }
195
+ })
196
+ },
197
+ bindRowResizeObserver () {
198
+ if (typeof ResizeObserver === 'undefined') return
199
+ this.unbindRowResizeObserver()
200
+ const row = this.getSidebarParentRow()
201
+ if (!row) return
202
+ this.rowResizeObserver = new ResizeObserver(() => {
203
+ this.scheduleSyncDrawerMinHeight()
204
+ })
205
+ this.rowResizeObserver.observe(row)
206
+ },
207
+ unbindRowResizeObserver () {
208
+ if (this.rowResizeObserver) {
209
+ this.rowResizeObserver.disconnect()
210
+ this.rowResizeObserver = null
211
+ }
212
+ },
213
+ bindDrawerResizeObserver () {
214
+ if (typeof ResizeObserver === 'undefined') return
215
+ this.unbindDrawerResizeObserver()
216
+ const root = this.$refs.drawerRoot
217
+ if (!root) return
218
+ this.drawerResizeObserver = new ResizeObserver(() => {
219
+ this.updateDrawerHeight()
220
+ })
221
+ this.drawerResizeObserver.observe(root)
222
+ },
223
+ unbindDrawerResizeObserver () {
224
+ if (this.drawerResizeObserver) {
225
+ this.drawerResizeObserver.disconnect()
226
+ this.drawerResizeObserver = null
227
+ }
228
+ },
229
+ updateDrawerHeight () {
230
+ const root = this.$refs.drawerRoot
231
+ if (!root) return
232
+ const h = root.offsetHeight || root.getBoundingClientRect().height
233
+ if (h > 0) {
234
+ this._drawerHeight = Math.round(h)
235
+ }
236
+ },
107
237
  // 通用的样式保护方法
108
238
  protectElementStyles (element, stylesToProtect = ['padding', 'padding-left', 'padding-right', 'padding-top', 'padding-bottom', 'margin', 'background-color', 'color', 'font-size', 'border']) {
109
239
  const protectedStyles = {}
@@ -145,10 +275,24 @@ export default {
145
275
  this.restoreElementStyles(element, protectedStyles)
146
276
  },
147
277
  toggleDrawer () {
278
+ // 展开前读一次行高(此时仍为展开状态),更新最大高度记录,确保收起后基准正确
279
+ if (!this.isOpen) {
280
+ const row = this.getSidebarParentRow()
281
+ if (row) {
282
+ const h = Math.round(row.offsetHeight || row.getBoundingClientRect().height)
283
+ if (h > 0) {
284
+ if (this._maxRowHeight === null || h > this._maxRowHeight) {
285
+ this._maxRowHeight = h
286
+ }
287
+ }
288
+ }
289
+ }
148
290
  this.isOpen = !this.isOpen
149
291
  this.$emit('on-drawer-change', this.isOpen)
150
292
  this.updateLayout(this.isOpen)
151
293
  if (this.isOpen) {
294
+ // 展开后行高可能微增,读一次最新值(只增不减)
295
+ this.syncDrawerMinHeightToParentRow()
152
296
  this.$nextTick(() => {
153
297
  this.updateCardBodyPadding()
154
298
  })
@@ -167,7 +311,8 @@ export default {
167
311
  },
168
312
  // 更新card-body的padding
169
313
  updateCardBodyPadding () {
170
- if (this.widthMode) { return }
314
+ // widthMode 为空时才需要处理
315
+ if (!this.widthMode) { return }
171
316
  this.$nextTick(() => {
172
317
  const cardBody = this.$el.querySelector('.ant-card-body')
173
318
  if (cardBody) {
@@ -202,8 +347,13 @@ export default {
202
347
  return []
203
348
  }
204
349
 
205
- // 修改这里 如果没有指定className 则获取全部
206
- const allCols = Array.from(row.children).filter(child => !className || child.className.includes(className))
350
+ // 如果没有指定 className,则返回该行下所有的 a-col 同级元素
351
+ const allCols = Array.from(row.children).filter(child => {
352
+ const isCol = child.className && child.className.includes && child.className.includes('ant-col')
353
+ if (!isCol) return false
354
+ if (!className) return true
355
+ return child.className.includes(className)
356
+ })
207
357
 
208
358
  // 过滤掉当前a-col,返回其他所有a-col
209
359
  return allCols.filter(col => col !== currentCol)
@@ -212,6 +362,21 @@ export default {
212
362
  return []
213
363
  }
214
364
  },
365
+ // 在同一行中查找距离当前侧边栏最近的内容列(从右往左找第一个 a-col)
366
+ findNearestContentCol (otherCols, currentCol) {
367
+ if (!otherCols || otherCols.length === 0 || !currentCol || !currentCol.parentNode) {
368
+ return null
369
+ }
370
+ let prev = currentCol.previousSibling
371
+ while (prev) {
372
+ if (otherCols.includes(prev)) {
373
+ return prev
374
+ }
375
+ prev = prev.previousSibling
376
+ }
377
+ // 如果没找到明显靠近的,就兜底返回第一个同级列
378
+ return otherCols[0] || null
379
+ },
215
380
  // 计算mainCol,currentCol可以设置的总宽度
216
381
  computeRemainingWidth (allElements, mainCol, currentCol) {
217
382
  if (!allElements || allElements.length === 0) {
@@ -254,7 +419,8 @@ export default {
254
419
  updateLayout (isOpen) {
255
420
  this.$nextTick(() => {
256
421
  try {
257
- const otherCols = this.getSiblingCols('ant-col-12')
422
+ // 获取当前行内所有与侧边栏同级的 a-col
423
+ const otherCols = this.getSiblingCols()
258
424
  if (otherCols.length > 0) {
259
425
  let currentCol = this.$el.parentNode
260
426
  while (currentCol && !currentCol.className.includes('ant-col')) {
@@ -281,9 +447,9 @@ export default {
281
447
  maxWidth: `${drawerWidth}px`,
282
448
  transition: 'all 0.3s'
283
449
  })
284
- if (otherCols.length === 1) {
285
- const mainCol = otherCols[0]
286
- const remainingWidth = this.computeRemainingWidth(this.getSiblingCols(undefined), mainCol, currentCol)
450
+ const mainCol = this.findNearestContentCol(otherCols, currentCol)
451
+ if (mainCol) {
452
+ const remainingWidth = this.computeRemainingWidth(this.getSiblingCols(), mainCol, currentCol)
287
453
 
288
454
  // 使用安全的方式设置布局样式
289
455
  this.safeSetLayoutStyles(mainCol, {
@@ -300,9 +466,9 @@ export default {
300
466
  maxWidth: `${drawerWidth}%`,
301
467
  transition: 'all 0.3s'
302
468
  })
303
- if (otherCols.length === 1) {
304
- const mainCol = otherCols[0]
305
- const remainingWidth = this.computeRemainingWidth(this.getSiblingCols(undefined), mainCol, currentCol)
469
+ const mainCol = this.findNearestContentCol(otherCols, currentCol)
470
+ if (mainCol) {
471
+ const remainingWidth = this.computeRemainingWidth(this.getSiblingCols(), mainCol, currentCol)
306
472
 
307
473
  // 使用安全的方式设置布局样式
308
474
  this.safeSetLayoutStyles(mainCol, {
@@ -316,8 +482,8 @@ export default {
316
482
  // 恢复默认:只移除我们设置的属性
317
483
  currentCol.style.removeProperty('flex')
318
484
  currentCol.style.removeProperty('max-width')
319
- if (otherCols.length === 1) {
320
- const mainCol = otherCols[0]
485
+ const mainCol = this.findNearestContentCol(otherCols, currentCol)
486
+ if (mainCol) {
321
487
  mainCol.style.removeProperty('flex')
322
488
  mainCol.style.removeProperty('max-width')
323
489
  }
@@ -332,9 +498,9 @@ export default {
332
498
  maxWidth: width,
333
499
  transition: 'all 0.3s'
334
500
  })
335
- if (otherCols.length === 1) {
336
- const mainCol = otherCols[0]
337
- const remainingWidth = this.computeRemainingWidth(this.getSiblingCols(undefined), mainCol, currentCol)
501
+ const mainCol = this.findNearestContentCol(otherCols, currentCol)
502
+ if (mainCol) {
503
+ const remainingWidth = this.computeRemainingWidth(this.getSiblingCols(), mainCol, currentCol)
338
504
 
339
505
  // 使用安全的方式设置布局样式
340
506
  this.safeSetLayoutStyles(mainCol, {
@@ -350,6 +516,7 @@ export default {
350
516
  } catch (error) {
351
517
  console.error('布局更新失败:', error)
352
518
  }
519
+ this.syncDrawerMinHeightToParentRow()
353
520
  })
354
521
  }
355
522
  },
@@ -363,6 +530,7 @@ export default {
363
530
  this.$nextTick(() => {
364
531
  this.updateLayout(true)
365
532
  this.updateCardBodyPadding()
533
+ this.syncDrawerMinHeightToParentRow()
366
534
  })
367
535
  }
368
536
  }
@@ -374,16 +542,17 @@ export default {
374
542
  },
375
543
  drawerStyle () {
376
544
  const isPx = this.widthMode === 'px'
377
- if (!this.isOpen) {
378
- // collapsed width
379
- return isPx
545
+ const base = !this.isOpen
546
+ ? isPx
380
547
  ? { width: `${this.collapsedWidth}px` }
381
548
  : { width: `${this.collapsedWidthPercent}%` }
549
+ : isPx
550
+ ? { width: `${this.expandedWidth}px`, display: 'flex', flexDirection: 'row', alignItems: 'stretch' }
551
+ : { width: `${this.expandedWidthPercent}%`, display: 'flex', flexDirection: 'row', alignItems: 'stretch' }
552
+ if (this.rowHeight) {
553
+ base['--sidebar-row-height'] = this.rowHeight + 'px'
382
554
  }
383
- // expanded width for the whole drawer wrapper
384
- return isPx
385
- ? { width: `${this.expandedWidth}px`, display: 'flex', flexDirection: 'row', alignItems: 'stretch' }
386
- : { width: `${this.expandedWidthPercent}%`, display: 'flex', flexDirection: 'row', alignItems: 'stretch' }
555
+ return base
387
556
  },
388
557
  contentStyle () {
389
558
  const isPx = this.widthMode === 'px'
@@ -395,15 +564,13 @@ export default {
395
564
  const contentPercent = Math.max(0, (this.expandedWidthPercent || 0) - (this.collapsedWidthPercent || 0))
396
565
  return { width: `${contentPercent}%`, flex: '1 1 auto', overflow: 'hidden' }
397
566
  },
398
- toggleStyle () {
399
- // 在展开时让切换按钮参与flex布局,避免与内容重叠;收起时保持绝对定位
567
+ toggleButtonStyle () {
400
568
  const isPx = this.widthMode === 'px'
401
- if (this.isOpen) {
402
- return isPx
403
- ? { position: 'relative', left: '0', zIndex: 1, flex: `0 0 ${this.collapsedWidth}px`, width: `${this.collapsedWidth}px` }
404
- : { position: 'relative', left: '0', zIndex: 1, flex: `0 0 ${this.collapsedWidthPercent}%`, width: `${this.collapsedWidthPercent}%` }
405
- }
406
- return { left: '0' }
569
+ const width = isPx ? `${this.collapsedWidth}px` : `${this.collapsedWidthPercent}%`
570
+ // 优先用外部传入的 rowHeight,否则用内部计算的行高
571
+ const rowH = this.rowHeight || this._maxRowHeight
572
+ const top = rowH ? `${rowH / 2 - 24}px` : '50%'
573
+ return { width, top }
407
574
  }
408
575
  }
409
576
  }
@@ -413,21 +580,29 @@ export default {
413
580
  .drawer {
414
581
  position: relative;
415
582
  height: 100%;
583
+ min-height: 100%;
416
584
  width: 100%;
417
585
  background-color: #fff;
418
586
  border-left: solid rgba(240, 242, 245) 2px;
419
- transition: all 0.3s;
587
+ /* 明确列出需要过渡的属性,min-height 和 height 永远不走过渡,避免箭头"慢慢下移" */
588
+ transition: width 0.3s ease, background-color 0.3s ease, border-color 0.3s ease,
589
+ box-shadow 0.3s ease, border-radius 0.3s ease;
420
590
  border-radius: 10px;
421
591
  }
422
-
592
+ /* 折叠时保持高度 */
423
593
  .drawer-collapsed {
424
594
  width: 26px;
425
595
  box-shadow: none;
596
+ height: var(--sidebar-row-height, 100px) !important;
597
+ min-height: var(--sidebar-row-height, 100px);
598
+ }
599
+ /* 展开时高度由内容撑开,但最小高度用同一变量保持箭头位置一致 */
600
+ .drawer:not(.drawer-collapsed) {
601
+ min-height: var(--sidebar-row-height, 100px);
426
602
  }
427
603
  .drawer-toggle {
428
604
  position: absolute;
429
- top: 50%;
430
- transform: translateY(-50%);
605
+ left: 0;
431
606
  width: 26px;
432
607
  height: 48px;
433
608
  cursor: pointer;
@@ -436,7 +611,6 @@ export default {
436
611
  align-items: center;
437
612
  justify-content: center;
438
613
  z-index: 1000;
439
- transition: all 0.3s;
440
614
  }
441
615
  /* 打开状态:箭头在抽屉内 */
442
616
  .drawer:not(.drawer-collapsed) .drawer-toggle {