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 +1 -1
- package/src/base-client/components/common/XMarkdownSectionExtractor/DemoXMarkdownSectionExtractor.vue +12 -108
- package/src/base-client/components/common/XMarkdownSectionExtractor/XMarkdownSectionExtractor.vue +98 -83
- package/src/base-client/components/his/XSidebar/XSidebar.vue +216 -42
package/package.json
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
package/src/base-client/components/common/XMarkdownSectionExtractor/XMarkdownSectionExtractor.vue
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="x-markdown-section-extractor">
|
|
3
|
-
<!--
|
|
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
|
-
<!--
|
|
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
|
-
.
|
|
254
|
-
padding:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
241
|
+
.section-block {
|
|
242
|
+
margin-bottom: 20px;
|
|
243
|
+
padding-bottom: 16px;
|
|
244
|
+
border-bottom: 1px dashed #e8e8e8;
|
|
266
245
|
|
|
267
|
-
&:
|
|
268
|
-
|
|
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
|
-
|
|
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="
|
|
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
|
-
|
|
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
|
-
//
|
|
206
|
-
const allCols = Array.from(row.children).filter(child =>
|
|
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
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
const remainingWidth = this.computeRemainingWidth(this.getSiblingCols(
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
const remainingWidth = this.computeRemainingWidth(this.getSiblingCols(
|
|
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
|
-
|
|
320
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
const remainingWidth = this.computeRemainingWidth(this.getSiblingCols(
|
|
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
|
-
|
|
378
|
-
|
|
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
|
-
|
|
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
|
-
|
|
399
|
-
// 在展开时让切换按钮参与flex布局,避免与内容重叠;收起时保持绝对定位
|
|
567
|
+
toggleButtonStyle () {
|
|
400
568
|
const isPx = this.widthMode === 'px'
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|