@c2-markdown-ui/vue 0.0.0-stage → 0.4.3
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/README.md +112 -2
- package/dist/MarkdownUI.vue.d.ts +15 -0
- package/dist/favicon.ico +0 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +10843 -0
- package/dist/index.umd.cjs +53 -0
- package/dist/widgets/Button.vue.d.ts +19 -0
- package/dist/widgets/ButtonGroup.vue.d.ts +13 -0
- package/dist/widgets/Cascader.vue.d.ts +48 -0
- package/dist/widgets/Chart.vue.d.ts +24 -0
- package/dist/widgets/DatePicker.vue.d.ts +42 -0
- package/dist/widgets/Form.vue.d.ts +59 -0
- package/dist/widgets/Incomplete.vue.d.ts +2 -0
- package/dist/widgets/MultipleChoiceQuestion.vue.d.ts +20 -0
- package/dist/widgets/NumberInput.vue.d.ts +37 -0
- package/dist/widgets/Quiz.vue.d.ts +39 -0
- package/dist/widgets/RadioGroup.vue.d.ts +43 -0
- package/dist/widgets/Select.vue.d.ts +27 -0
- package/dist/widgets/SelectMulti.vue.d.ts +38 -0
- package/dist/widgets/ShortAnswerQuestion.vue.d.ts +22 -0
- package/dist/widgets/Slider.vue.d.ts +21 -0
- package/dist/widgets/Switch.vue.d.ts +28 -0
- package/dist/widgets/Text.vue.d.ts +22 -0
- package/dist/widgets/TextInput.vue.d.ts +32 -0
- package/dist/widgets/TreeSelect.vue.d.ts +109 -0
- package/dist/widgets/Widget.vue.d.ts +8 -0
- package/dist/widgets/chartRemoteData.d.ts +17 -0
- package/dist/widgets/index.d.ts +20 -0
- package/dist/widgets/remoteChoices.d.ts +22 -0
- package/dist/widgets/treeSelectTypes.d.ts +43 -0
- package/package.json +46 -4
- package/src/App.vue +11 -0
- package/src/lib/MarkdownUI.vue +105 -0
- package/src/lib/index.ts +28 -0
- package/src/lib/widgets/Button.vue +47 -0
- package/src/lib/widgets/ButtonGroup.vue +43 -0
- package/src/lib/widgets/Cascader.vue +475 -0
- package/src/lib/widgets/Chart.vue +287 -0
- package/src/lib/widgets/DatePicker.vue +659 -0
- package/src/lib/widgets/Form.vue +479 -0
- package/src/lib/widgets/Incomplete.vue +56 -0
- package/src/lib/widgets/MultipleChoiceQuestion.vue +73 -0
- package/src/lib/widgets/NumberInput.vue +179 -0
- package/src/lib/widgets/Quiz.vue +205 -0
- package/src/lib/widgets/RadioGroup.vue +237 -0
- package/src/lib/widgets/Select.vue +279 -0
- package/src/lib/widgets/SelectMulti.vue +145 -0
- package/src/lib/widgets/ShortAnswerQuestion.vue +79 -0
- package/src/lib/widgets/Slider.vue +61 -0
- package/src/lib/widgets/Switch.vue +68 -0
- package/src/lib/widgets/Text.vue +59 -0
- package/src/lib/widgets/TextInput.vue +83 -0
- package/src/lib/widgets/TreeSelect.vue +872 -0
- package/src/lib/widgets/Widget.vue +185 -0
- package/src/lib/widgets/chartRemoteData.ts +84 -0
- package/src/lib/widgets/index.ts +20 -0
- package/src/lib/widgets/remoteChoices.ts +64 -0
- package/src/lib/widgets/treeSelectTypes.ts +49 -0
- package/src/lib/widgets.css +1754 -0
- package/src/main.ts +4 -0
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="selector widget-tree-select" ref="rootEl">
|
|
3
|
+
<div v-if="label || hint || required" class="widget-label-row">
|
|
4
|
+
<span v-if="required" class="widget-required-mark" aria-hidden="true">*</span>
|
|
5
|
+
<label v-if="label">{{ label }}</label>
|
|
6
|
+
<span v-if="hint" class="widget-hint" :data-hint="hint">ⓘ</span>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="widget-field-content">
|
|
9
|
+
<!-- 触发按钮 -->
|
|
10
|
+
<button
|
|
11
|
+
type="button"
|
|
12
|
+
class="ts-input"
|
|
13
|
+
:class="{ 'widget-input-error': !!error }"
|
|
14
|
+
:disabled="disabled"
|
|
15
|
+
:aria-expanded="open"
|
|
16
|
+
:data-full="allTagsText"
|
|
17
|
+
@click="toggleOpen"
|
|
18
|
+
>
|
|
19
|
+
<!-- 多选 tag 显示(按回显策略过滤 + maxTagCount 截断) -->
|
|
20
|
+
<template v-if="multiple && selected.length > 0">
|
|
21
|
+
<span class="ts-tag" v-for="v in displayTags" :key="v.value">
|
|
22
|
+
{{ v.label }}
|
|
23
|
+
</span>
|
|
24
|
+
<span v-if="strategyValues.length > tagLimit" class="ts-tag ts-tag-more">(+{{ strategyValues.length - tagLimit }})</span>
|
|
25
|
+
</template>
|
|
26
|
+
<span
|
|
27
|
+
v-else
|
|
28
|
+
class="ts-input-text"
|
|
29
|
+
:class="{ 'ts-placeholder': selected.length === 0 }"
|
|
30
|
+
>
|
|
31
|
+
{{ selectedLabel || placeholder || '请选择' }}
|
|
32
|
+
</span>
|
|
33
|
+
<span
|
|
34
|
+
v-if="!disabled && selected.length > 0"
|
|
35
|
+
class="ts-clear"
|
|
36
|
+
role="button"
|
|
37
|
+
tabindex="-1"
|
|
38
|
+
aria-label="清空"
|
|
39
|
+
@click.stop="clearValue"
|
|
40
|
+
>×</span>
|
|
41
|
+
<span v-else-if="!disabled" class="ts-arrow" :class="{ 'ts-arrow-open': open }" aria-hidden="true">▾</span>
|
|
42
|
+
</button>
|
|
43
|
+
<div v-if="error" class="widget-error-text" role="alert">{{ error }}</div>
|
|
44
|
+
|
|
45
|
+
<!-- 下拉面板 -->
|
|
46
|
+
<div v-if="open" class="ts-panel">
|
|
47
|
+
<!-- 搜索框 -->
|
|
48
|
+
<div v-if="searchable" class="ts-search">
|
|
49
|
+
<input
|
|
50
|
+
ref="searchInputEl"
|
|
51
|
+
v-model="query"
|
|
52
|
+
type="text"
|
|
53
|
+
class="ts-search-input"
|
|
54
|
+
placeholder="搜索…"
|
|
55
|
+
@keydown.esc="open = false"
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
<!-- 工具栏 -->
|
|
59
|
+
<div class="ts-toolbar">
|
|
60
|
+
<button type="button" class="ts-tool-btn" @click="expandAll">展开全部</button>
|
|
61
|
+
<button type="button" class="ts-tool-btn" @click="collapseAll">折叠全部</button>
|
|
62
|
+
</div>
|
|
63
|
+
<!-- 树形节点列表 -->
|
|
64
|
+
<div class="ts-tree" role="tree">
|
|
65
|
+
<template v-if="visibleNodes.length > 0">
|
|
66
|
+
<TreeNode
|
|
67
|
+
v-for="node in visibleNodes"
|
|
68
|
+
:key="node.value"
|
|
69
|
+
:node="node"
|
|
70
|
+
:depth="node.depth"
|
|
71
|
+
:multiple="multiple"
|
|
72
|
+
:checked="isNodeChecked(node)"
|
|
73
|
+
:indeterminate="isNodeIndeterminate(node)"
|
|
74
|
+
:treeLine="treeLine"
|
|
75
|
+
:lazy="lazy"
|
|
76
|
+
:url="url"
|
|
77
|
+
:remoteData="remoteData"
|
|
78
|
+
@toggle="handleToggle"
|
|
79
|
+
@check="handleCheck"
|
|
80
|
+
@load="handleLoad"
|
|
81
|
+
/>
|
|
82
|
+
</template>
|
|
83
|
+
<div v-else class="ts-empty">{{ loading ? '加载中…' : '暂无数据' }}</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</template>
|
|
89
|
+
|
|
90
|
+
<script setup lang="ts">
|
|
91
|
+
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
|
92
|
+
import type { TreeNodeRaw, TreeSelectNode as TsNode, TreeSelectProps } from './treeSelectTypes'
|
|
93
|
+
|
|
94
|
+
type Node = TsNode
|
|
95
|
+
|
|
96
|
+
// ---------- 类型 ----------
|
|
97
|
+
|
|
98
|
+
const props = withDefaults(defineProps<TreeSelectProps>(), {
|
|
99
|
+
choices: () => [],
|
|
100
|
+
data: undefined,
|
|
101
|
+
url: '',
|
|
102
|
+
lazy: false,
|
|
103
|
+
multiple: false,
|
|
104
|
+
checkStrictly: false,
|
|
105
|
+
showCheckedStrategy: 'all',
|
|
106
|
+
maxTagCount: undefined,
|
|
107
|
+
treeLine: false,
|
|
108
|
+
label: '',
|
|
109
|
+
default: '',
|
|
110
|
+
placeholder: '',
|
|
111
|
+
hint: '',
|
|
112
|
+
required: false,
|
|
113
|
+
message: '',
|
|
114
|
+
error: '',
|
|
115
|
+
disabled: false,
|
|
116
|
+
searchable: false
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const emit = defineEmits<{
|
|
120
|
+
change: [value: string | string[]]
|
|
121
|
+
}>()
|
|
122
|
+
|
|
123
|
+
// ---------- 数据转换 ----------
|
|
124
|
+
function parseSeg(seg: string) {
|
|
125
|
+
const idx = seg.indexOf(':')
|
|
126
|
+
if (idx > 0) {
|
|
127
|
+
const v = seg.slice(0, idx).trim()
|
|
128
|
+
const l = seg.slice(idx + 1).trim()
|
|
129
|
+
if (v && l) return { value: v, label: l }
|
|
130
|
+
}
|
|
131
|
+
return { value: seg.trim(), label: seg.trim() }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function rawToTree(nodes: TreeNodeRaw[] | undefined, depth = 0): Node[] {
|
|
135
|
+
if (!Array.isArray(nodes)) return []
|
|
136
|
+
return nodes.map(n => {
|
|
137
|
+
const v = n.value ?? n.key
|
|
138
|
+
const children = n.children
|
|
139
|
+
const hasChildren = Array.isArray(children) && children.length > 0
|
|
140
|
+
const wasLoaded = n.loaded === true
|
|
141
|
+
return {
|
|
142
|
+
value: String(v ?? ''),
|
|
143
|
+
label: String(n.label ?? v ?? ''),
|
|
144
|
+
children: rawToTree(children, depth + 1),
|
|
145
|
+
loaded: hasChildren || wasLoaded,
|
|
146
|
+
depth
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildTreeFromChoices(choices: string[]): Node[] {
|
|
152
|
+
const root: Node[] = []
|
|
153
|
+
for (const path of choices) {
|
|
154
|
+
if (!path) continue
|
|
155
|
+
const segs = path.split('/').map(s => s.trim()).filter(Boolean)
|
|
156
|
+
if (segs.length === 0) continue
|
|
157
|
+
let level = root
|
|
158
|
+
for (let i = 0; i < segs.length; i++) {
|
|
159
|
+
const { value, label } = parseSeg(segs[i])
|
|
160
|
+
let node = level.find(n => n.value === value)
|
|
161
|
+
if (!node) {
|
|
162
|
+
node = { value, label, children: [], loaded: false, depth: i }
|
|
163
|
+
level.push(node)
|
|
164
|
+
}
|
|
165
|
+
level = node.children
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return root
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function normalizeRemoteTree(json: any): TreeNodeRaw[] {
|
|
172
|
+
let list = json
|
|
173
|
+
if (!Array.isArray(json)) {
|
|
174
|
+
list = Array.isArray(json?.data) ? json.data
|
|
175
|
+
: Array.isArray(json?.options) ? json.options
|
|
176
|
+
: Array.isArray(json?.choices) ? json.choices
|
|
177
|
+
: Array.isArray(json?.items) ? json.items
|
|
178
|
+
: Array.isArray(json?.tree) ? json.tree
|
|
179
|
+
: Array.isArray(json?.treeData) ? json.treeData
|
|
180
|
+
: []
|
|
181
|
+
}
|
|
182
|
+
const map = (arr: any[]): TreeNodeRaw[] =>
|
|
183
|
+
(arr || [])
|
|
184
|
+
.map((it: any): TreeNodeRaw | null => {
|
|
185
|
+
if (!it || typeof it !== 'object') return null
|
|
186
|
+
const value = it.value ?? it.key
|
|
187
|
+
if (value === undefined || value === null || value === '') return null
|
|
188
|
+
const node: TreeNodeRaw = { value: String(value) }
|
|
189
|
+
const label = it.label ?? it.name ?? it.text
|
|
190
|
+
if (label !== undefined && label !== null) node.label = String(label)
|
|
191
|
+
if (Array.isArray(it.children) && it.children.length > 0) {
|
|
192
|
+
node.children = map(it.children)
|
|
193
|
+
}
|
|
194
|
+
return node
|
|
195
|
+
})
|
|
196
|
+
.filter((n: TreeNodeRaw | null): n is TreeNodeRaw => n !== null)
|
|
197
|
+
return map(list)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------- 状态 ----------
|
|
201
|
+
const remoteData = ref<TreeNodeRaw[] | null>(null)
|
|
202
|
+
const loading = ref(false)
|
|
203
|
+
const expanded = ref<Set<string>>(new Set())
|
|
204
|
+
const selected = ref<string[]>([])
|
|
205
|
+
const query = ref('')
|
|
206
|
+
const open = ref(false)
|
|
207
|
+
const searchInputEl = ref<HTMLInputElement | null>(null)
|
|
208
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
209
|
+
|
|
210
|
+
// ---------- 树数据源 ----------
|
|
211
|
+
const baseTree = computed<Node[]>(() => {
|
|
212
|
+
if (remoteData.value) return rawToTree(remoteData.value)
|
|
213
|
+
if (Array.isArray(props.data) && props.data.length > 0) return rawToTree(props.data)
|
|
214
|
+
return buildTreeFromChoices(props.choices)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
const searchTree = computed<Node[]>(() => {
|
|
218
|
+
if (!query.value.trim()) return baseTree.value
|
|
219
|
+
const q = query.value.toLowerCase()
|
|
220
|
+
return pruneTree(baseTree.value, q)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
function pruneTree(nodes: Node[], q: string): Node[] {
|
|
224
|
+
const result: Node[] = []
|
|
225
|
+
for (const node of nodes) {
|
|
226
|
+
const matchLabel = node.label.toLowerCase().includes(q)
|
|
227
|
+
const childMatch = pruneTree(node.children, q)
|
|
228
|
+
if (matchLabel || childMatch.length > 0) {
|
|
229
|
+
result.push({
|
|
230
|
+
...node,
|
|
231
|
+
children: childMatch.length > 0 ? childMatch : node.children,
|
|
232
|
+
loaded: childMatch.length > 0 ? true : node.loaded
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return result
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------- 可见节点扁平列表 ----------
|
|
240
|
+
const visibleNodes = computed<Node[]>(() => {
|
|
241
|
+
const nodes: Node[] = []
|
|
242
|
+
function walk(list: Node[]) {
|
|
243
|
+
for (const node of list) {
|
|
244
|
+
nodes.push(node)
|
|
245
|
+
if (expanded.value.has(node.value) && node.children.length > 0) {
|
|
246
|
+
walk(node.children)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
walk(searchTree.value)
|
|
251
|
+
return nodes
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
// ---------- 展示文案 ----------
|
|
255
|
+
const selectedLabel = computed(() => {
|
|
256
|
+
const v = selected.value[0]
|
|
257
|
+
if (!v) return ''
|
|
258
|
+
function find(n: Node[]): string {
|
|
259
|
+
for (const node of n) {
|
|
260
|
+
if (node.value === v) return node.label
|
|
261
|
+
const f = find(node.children)
|
|
262
|
+
if (f) return f
|
|
263
|
+
}
|
|
264
|
+
return ''
|
|
265
|
+
}
|
|
266
|
+
return find(baseTree.value)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
const displayTags = computed<{ value: string; label: string }[]>(() => {
|
|
270
|
+
return strategyValues.value.slice(0, tagLimit.value).map(v => ({
|
|
271
|
+
value: v,
|
|
272
|
+
label: findLabel(v)
|
|
273
|
+
}))
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
function findLabel(v: string): string {
|
|
277
|
+
function find(n: Node[]): string {
|
|
278
|
+
for (const node of n) {
|
|
279
|
+
if (node.value === v) return node.label
|
|
280
|
+
const f = find(node.children)
|
|
281
|
+
if (f) return f
|
|
282
|
+
}
|
|
283
|
+
return v
|
|
284
|
+
}
|
|
285
|
+
return find(baseTree.value)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function findNode(value: string, nodes: Node[] = baseTree.value): Node | undefined {
|
|
289
|
+
for (const n of nodes) {
|
|
290
|
+
if (n.value === value) return n
|
|
291
|
+
const found = findNode(value, n.children)
|
|
292
|
+
if (found) return found
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** 触发器最多显示的标签数(antd maxTagCount),不传则不限制 */
|
|
297
|
+
const tagLimit = computed<number>(() => {
|
|
298
|
+
return typeof props.maxTagCount === 'number' && props.maxTagCount >= 0 ? props.maxTagCount : Infinity
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
/** 全部已选项文本(策略过滤后),悬浮提示展示用 */
|
|
302
|
+
const allTagsText = computed<string>(() => {
|
|
303
|
+
if (!props.multiple || selected.value.length === 0) return ''
|
|
304
|
+
return strategyValues.value.map(v => findLabel(v)).join(', ')
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
/** 按回显策略(antd showCheckedStrategy)过滤后的选中值:all 全部 / parent 仅顶层父级 / child 仅叶子 */
|
|
308
|
+
const strategyValues = computed<string[]>(() => {
|
|
309
|
+
const values = selected.value
|
|
310
|
+
if (!props.multiple || props.checkStrictly || props.showCheckedStrategy === 'all') return values
|
|
311
|
+
const set = new Set(values)
|
|
312
|
+
if (props.showCheckedStrategy === 'child') {
|
|
313
|
+
return values.filter(v => {
|
|
314
|
+
const n = findNode(v)
|
|
315
|
+
return !n || n.children.length === 0
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
// parent:级联模式下勾满的父级回显为父级本身,其全部后代不单独显示;
|
|
319
|
+
// 未勾满的父级不显示,只显示其下被选中的叶子/子父级。
|
|
320
|
+
// 注意"父级全选"不能只看 set.has(父级value) —— 级联勾子节点时 selected 里
|
|
321
|
+
// 只有叶子值,父级 value 不在其中,需按"所有后代均被选中"判定
|
|
322
|
+
const result: string[] = []
|
|
323
|
+
const fullyChecked = (n: Node): boolean =>
|
|
324
|
+
n.children.length === 0 ? set.has(n.value) : n.children.every(fullyChecked)
|
|
325
|
+
const walk = (nodes: Node[]) => {
|
|
326
|
+
for (const n of nodes) {
|
|
327
|
+
if (n.children.length > 0 && fullyChecked(n)) {
|
|
328
|
+
// 全选父级:回显自身,跳过后代
|
|
329
|
+
result.push(n.value)
|
|
330
|
+
} else {
|
|
331
|
+
walk(n.children)
|
|
332
|
+
if (n.children.length === 0 && set.has(n.value)) result.push(n.value)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
walk(baseTree.value)
|
|
337
|
+
// selected 中可能存在树外的值(如懒加载未加载的分支),原样保留
|
|
338
|
+
const known = new Set<string>()
|
|
339
|
+
const collect = (nodes: Node[]) => {
|
|
340
|
+
for (const n of nodes) { known.add(n.value); collect(n.children) }
|
|
341
|
+
}
|
|
342
|
+
collect(baseTree.value)
|
|
343
|
+
for (const v of values) if (!known.has(v)) result.push(v)
|
|
344
|
+
return result
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
// ---------- 搜索时自动展开匹配祖先 ----------
|
|
348
|
+
watch(query, (q) => {
|
|
349
|
+
if (!q.trim()) return
|
|
350
|
+
const qLower = q.toLowerCase()
|
|
351
|
+
const toExpand = new Set<string>()
|
|
352
|
+
function collectMatches(nodes: Node[], ancestors: string[]) {
|
|
353
|
+
for (const node of nodes) {
|
|
354
|
+
if (node.label.toLowerCase().includes(qLower)) {
|
|
355
|
+
ancestors.forEach(a => toExpand.add(a))
|
|
356
|
+
}
|
|
357
|
+
if (node.children.length > 0) {
|
|
358
|
+
collectMatches(node.children, [...ancestors, node.value])
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
collectMatches(baseTree.value, [])
|
|
363
|
+
toExpand.forEach(v => expanded.value.add(v))
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
// ---------- 交互 ----------
|
|
367
|
+
function toggleOpen() {
|
|
368
|
+
if (props.disabled) return
|
|
369
|
+
open.value = !open.value
|
|
370
|
+
if (open.value && props.searchable) {
|
|
371
|
+
query.value = ''
|
|
372
|
+
nextTick(() => searchInputEl.value?.focus())
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function handleToggle(value: string, hasChildren: boolean) {
|
|
377
|
+
if (hasChildren) {
|
|
378
|
+
const s = new Set(expanded.value)
|
|
379
|
+
if (s.has(value)) s.delete(value)
|
|
380
|
+
else s.add(value)
|
|
381
|
+
expanded.value = s
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** 节点是否被全选:checkStrictly 下仅看自身;关联模式下所有后代都被选中 */
|
|
386
|
+
function isNodeChecked(node: Node): boolean {
|
|
387
|
+
if (props.checkStrictly || !props.multiple) return selected.value.includes(node.value)
|
|
388
|
+
if (node.children.length === 0) return selected.value.includes(node.value)
|
|
389
|
+
return node.children.every(c => isNodeChecked(c))
|
|
390
|
+
}
|
|
391
|
+
/** 节点是否被半选(部分后代被选中,仅关联模式非叶子有意义) */
|
|
392
|
+
function isNodeIndeterminate(node: Node): boolean {
|
|
393
|
+
if (props.checkStrictly || node.children.length === 0) return false
|
|
394
|
+
const childStates = node.children.map(c => isNodeChecked(c) || isNodeIndeterminate(c))
|
|
395
|
+
return childStates.some(Boolean) && !childStates.every(Boolean)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function handleCheck(node: Node) {
|
|
399
|
+
touched = true
|
|
400
|
+
if (props.multiple) {
|
|
401
|
+
if (props.checkStrictly) {
|
|
402
|
+
// 父子不关联:只切换单节点
|
|
403
|
+
selected.value = selected.value.includes(node.value)
|
|
404
|
+
? selected.value.filter(v => v !== node.value)
|
|
405
|
+
: [...selected.value, node.value]
|
|
406
|
+
} else {
|
|
407
|
+
const set = new Set(selected.value)
|
|
408
|
+
if (set.has(node.value)) set.delete(node.value)
|
|
409
|
+
else set.add(node.value)
|
|
410
|
+
// 全选/取消子孙
|
|
411
|
+
function walkDescendants(n: Node, check: boolean) {
|
|
412
|
+
if (check) set.add(n.value)
|
|
413
|
+
else set.delete(n.value)
|
|
414
|
+
n.children.forEach(c => walkDescendants(c, check))
|
|
415
|
+
}
|
|
416
|
+
const checked = set.has(node.value)
|
|
417
|
+
walkDescendants(node, checked)
|
|
418
|
+
selected.value = Array.from(set)
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
selected.value = [node.value]
|
|
422
|
+
open.value = false
|
|
423
|
+
}
|
|
424
|
+
emit('change', props.multiple ? strategyValues.value : selected.value[0] ?? '')
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** 查找从根到目标节点的 value 路径(懒加载 fill 按路径逐级定位挂载点) */
|
|
428
|
+
function nodePathOf(target: string, nodes: Node[] = baseTree.value): string[] | null {
|
|
429
|
+
for (const n of nodes) {
|
|
430
|
+
if (n.value === target) return [n.value]
|
|
431
|
+
if (n.children.length > 0) {
|
|
432
|
+
const sub = nodePathOf(target, n.children)
|
|
433
|
+
if (sub) return [n.value, ...sub]
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return null
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function handleLoad(node: Node) {
|
|
440
|
+
if (!props.url || node.loaded) return
|
|
441
|
+
loading.value = true
|
|
442
|
+
try {
|
|
443
|
+
const sep = props.url.includes('?') ? '&' : '?'
|
|
444
|
+
const res = await fetch(`${props.url}${sep}parent=${encodeURIComponent(node.value)}`)
|
|
445
|
+
if (!res.ok) throw new Error()
|
|
446
|
+
const json = await res.json()
|
|
447
|
+
const list = normalizeRemoteTree(json)
|
|
448
|
+
// 注入子级到 remoteData(按从根到节点的完整路径定位挂载点)
|
|
449
|
+
function fill(nodes: TreeNodeRaw[], path: string[], children: TreeNodeRaw[]): TreeNodeRaw[] {
|
|
450
|
+
return nodes.map(n => {
|
|
451
|
+
const v = String(n.value ?? n.key ?? '')
|
|
452
|
+
if (v !== path[0]) return n
|
|
453
|
+
if (path.length === 1) return { ...n, children, loaded: true }
|
|
454
|
+
return { ...n, children: fill(n.children || [], path.slice(1), children) }
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
if (remoteData.value) {
|
|
458
|
+
const path = nodePathOf(node.value) ?? [node.value]
|
|
459
|
+
remoteData.value = fill(remoteData.value, path, list)
|
|
460
|
+
}
|
|
461
|
+
} catch {
|
|
462
|
+
// 静默失败
|
|
463
|
+
} finally {
|
|
464
|
+
loading.value = false
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function expandAll() {
|
|
469
|
+
const all = new Set<string>()
|
|
470
|
+
function collect(nodes: Node[]) {
|
|
471
|
+
for (const n of nodes) {
|
|
472
|
+
if (n.children.length > 0) {
|
|
473
|
+
all.add(n.value)
|
|
474
|
+
collect(n.children)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
collect(baseTree.value)
|
|
479
|
+
expanded.value = all
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function collapseAll() {
|
|
483
|
+
expanded.value = new Set()
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function clearValue() {
|
|
487
|
+
touched = true
|
|
488
|
+
selected.value = []
|
|
489
|
+
emit('change', props.multiple ? [] : '')
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ---------- 初始化选中值 ----------
|
|
493
|
+
/** 默认值语义(对齐 antd TreeSelect defaultValue):
|
|
494
|
+
* 逗号分隔多值;每个值支持 / 分隔路径(zj/hz)或节点 value(hz),
|
|
495
|
+
* 路径写法按段逐级查找(段先按 value 匹配、再按 label 匹配),定位到路径末端节点;
|
|
496
|
+
* 单段 value 在根级找不到时,从整棵树深度查找(antd 语义:value 全树唯一) */
|
|
497
|
+
function resolveDefault(treeNodes: Node[]): { values: string[]; ancestors: string[] } {
|
|
498
|
+
const def = props.default
|
|
499
|
+
if (!def) return { values: [], ancestors: [] }
|
|
500
|
+
const paths = Array.isArray(def)
|
|
501
|
+
? def.map(String)
|
|
502
|
+
: String(def).split(',').map(s => s.trim()).filter(Boolean)
|
|
503
|
+
const values: string[] = []
|
|
504
|
+
const ancestors: string[] = []
|
|
505
|
+
/** 从整棵树深度查找节点,返回从根到命中节点的 value 链 */
|
|
506
|
+
function deepFind(nodes: Node[], seg: string, chain: string[]): string[] | null {
|
|
507
|
+
for (const n of nodes) {
|
|
508
|
+
if (n.value === seg || n.label === seg) return [...chain, n.value]
|
|
509
|
+
const sub = deepFind(n.children, seg, [...chain, n.value])
|
|
510
|
+
if (sub) return sub
|
|
511
|
+
}
|
|
512
|
+
return null
|
|
513
|
+
}
|
|
514
|
+
for (const path of paths) {
|
|
515
|
+
const segs = path.split('/').map(s => s.trim()).filter(Boolean)
|
|
516
|
+
let level = treeNodes
|
|
517
|
+
let node: Node | undefined
|
|
518
|
+
const chain: string[] = []
|
|
519
|
+
for (const seg of segs) {
|
|
520
|
+
const found = level.find(n => n.value === seg) ?? level.find(n => n.label === seg)
|
|
521
|
+
if (!found) { node = undefined; break }
|
|
522
|
+
chain.push(found.value)
|
|
523
|
+
node = found
|
|
524
|
+
level = found.children
|
|
525
|
+
}
|
|
526
|
+
if (node) {
|
|
527
|
+
values.push(node.value)
|
|
528
|
+
ancestors.push(...chain.slice(0, -1))
|
|
529
|
+
} else if (segs.length === 1) {
|
|
530
|
+
// 单段 value 逐级未命中(根级找不到):从整棵树深度查找(antd defaultValue 语义)
|
|
531
|
+
const deep = deepFind(treeNodes, segs[0], [])
|
|
532
|
+
if (deep) {
|
|
533
|
+
values.push(deep[deep.length - 1])
|
|
534
|
+
ancestors.push(...deep.slice(0, -1))
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
values: [...new Set(values)],
|
|
540
|
+
ancestors: [...new Set(ancestors)]
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** 用户是否交互过(勾选/清空):交互后不再跟随 default 变化,避免覆盖用户选择 */
|
|
545
|
+
let touched = false
|
|
546
|
+
|
|
547
|
+
// 挂载/数据源就绪/default 变化时回填默认值(含祖先展开)
|
|
548
|
+
watch(
|
|
549
|
+
() => [props.default, baseTree.value] as const,
|
|
550
|
+
() => {
|
|
551
|
+
if (touched) return
|
|
552
|
+
const { values, ancestors } = resolveDefault(baseTree.value)
|
|
553
|
+
selected.value = values
|
|
554
|
+
const s = new Set(expanded.value)
|
|
555
|
+
ancestors.forEach(a => s.add(a))
|
|
556
|
+
expanded.value = s
|
|
557
|
+
},
|
|
558
|
+
{ immediate: true }
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
// ---------- 远端加载 ----------
|
|
562
|
+
if (props.url) {
|
|
563
|
+
let cancelled = false
|
|
564
|
+
loading.value = true
|
|
565
|
+
;(async () => {
|
|
566
|
+
try {
|
|
567
|
+
const res = await fetch(props.url)
|
|
568
|
+
if (!res.ok) throw new Error()
|
|
569
|
+
const json = await res.json()
|
|
570
|
+
if (cancelled) return
|
|
571
|
+
remoteData.value = normalizeRemoteTree(json)
|
|
572
|
+
} catch {
|
|
573
|
+
// 静默失败
|
|
574
|
+
} finally {
|
|
575
|
+
if (!cancelled) loading.value = false
|
|
576
|
+
}
|
|
577
|
+
})()
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ---------- 点击外部关闭 ----------
|
|
581
|
+
function onDocClick(e: MouseEvent) {
|
|
582
|
+
if (rootEl.value && !rootEl.value.contains(e.target as globalThis.Node)) {
|
|
583
|
+
open.value = false
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
onMounted(() => document.addEventListener('mousedown', onDocClick))
|
|
587
|
+
onBeforeUnmount(() => document.removeEventListener('mousedown', onDocClick))
|
|
588
|
+
</script>
|
|
589
|
+
|
|
590
|
+
<script lang="ts">
|
|
591
|
+
// 子组件:单个树节点(避免模板递归,改用递归渲染)
|
|
592
|
+
import { defineComponent, h, type VNode } from 'vue'
|
|
593
|
+
import type { TreeSelectNode } from './treeSelectTypes'
|
|
594
|
+
|
|
595
|
+
const TreeNode = defineComponent({
|
|
596
|
+
name: 'TreeNode',
|
|
597
|
+
props: {
|
|
598
|
+
node: { type: Object as () => TreeSelectNode, required: true },
|
|
599
|
+
depth: { type: Number, required: true },
|
|
600
|
+
multiple: { type: Boolean, default: false },
|
|
601
|
+
checked: { type: Boolean, default: false },
|
|
602
|
+
indeterminate: { type: Boolean, default: false },
|
|
603
|
+
treeLine: { type: Boolean, default: false },
|
|
604
|
+
lazy: { type: Boolean, default: false },
|
|
605
|
+
url: { type: String, default: '' }
|
|
606
|
+
},
|
|
607
|
+
emits: ['toggle', 'check', 'load'],
|
|
608
|
+
setup(props, { emit }) {
|
|
609
|
+
return () => {
|
|
610
|
+
const { node, depth, multiple, checked, indeterminate, treeLine } = props
|
|
611
|
+
// 懒加载模式下,未加载子级的节点(children 为空且 loaded=false)视为"可展开",
|
|
612
|
+
// 显示展开箭头;加载完成返回空数组时 loaded=true,确认为真叶子
|
|
613
|
+
const isLazyUnloaded = props.lazy && !!props.url && !node.loaded && node.children.length === 0
|
|
614
|
+
const hasChildren = node.children.length > 0 || isLazyUnloaded
|
|
615
|
+
const paddingLeft = 16 + depth * 20
|
|
616
|
+
|
|
617
|
+
const labelEl = h('span', { class: 'ts-node-label', style: { paddingLeft: '8px' } }, node.label)
|
|
618
|
+
|
|
619
|
+
let leftEl: VNode
|
|
620
|
+
if (multiple) {
|
|
621
|
+
// 子组件内部元素不受 scoped 样式影响,选中/半选状态色用内联样式保证
|
|
622
|
+
leftEl = h('span', {
|
|
623
|
+
class: ['ts-checkbox', checked ? 'ts-checked' : '', indeterminate ? 'ts-indeterminate' : ''].filter(Boolean),
|
|
624
|
+
role: 'checkbox',
|
|
625
|
+
'aria-checked': indeterminate ? 'mixed' : checked ? 'true' : 'false',
|
|
626
|
+
style: (checked || indeterminate) ? { color: '#1677ff' } : undefined
|
|
627
|
+
}, checked ? '☑' : indeterminate ? '▣' : '☐')
|
|
628
|
+
} else {
|
|
629
|
+
leftEl = h('span', {
|
|
630
|
+
class: ['ts-radio', checked ? 'ts-radio-checked' : ''].filter(Boolean),
|
|
631
|
+
role: 'radio',
|
|
632
|
+
'aria-checked': checked ? 'true' : 'false',
|
|
633
|
+
style: checked ? { color: '#1677ff' } : undefined
|
|
634
|
+
}, checked ? '●' : '○')
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const arrowEl = hasChildren
|
|
638
|
+
? h('span', {
|
|
639
|
+
class: 'ts-node-arrow',
|
|
640
|
+
'aria-hidden': 'true',
|
|
641
|
+
onClick: (e: MouseEvent) => { e.stopPropagation(); emit('toggle', node.value, true) }
|
|
642
|
+
}, '▸')
|
|
643
|
+
: h('span', { class: 'ts-node-leaf', 'aria-hidden': 'true' }, '·')
|
|
644
|
+
|
|
645
|
+
return h('div', {
|
|
646
|
+
class: ['ts-node', treeLine ? 'ts-tree-line' : ''].filter(Boolean),
|
|
647
|
+
role: 'treeitem',
|
|
648
|
+
style: { paddingLeft: `${paddingLeft}px` },
|
|
649
|
+
onClick: () => {
|
|
650
|
+
if (hasChildren) {
|
|
651
|
+
emit('toggle', node.value, true)
|
|
652
|
+
// 懒加载未加载子级:单击展开时同步请求子级(对齐 React 版 handleNodeClick 行为)
|
|
653
|
+
if (isLazyUnloaded) emit('load', node)
|
|
654
|
+
}
|
|
655
|
+
// 单选模式下点击非叶子节点只展开不选中(避免关闭下拉);多选模式保持展开+勾选
|
|
656
|
+
if (!hasChildren || multiple) emit('check', node)
|
|
657
|
+
},
|
|
658
|
+
onDblclick: () => {
|
|
659
|
+
if (props.lazy && props.url && hasChildren && !node.loaded) {
|
|
660
|
+
emit('load', node)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}, [leftEl, arrowEl, labelEl])
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
export { TreeNode }
|
|
669
|
+
export default {}
|
|
670
|
+
</script>
|
|
671
|
+
|
|
672
|
+
<style scoped>
|
|
673
|
+
.widget-tree-select { position: relative; }
|
|
674
|
+
|
|
675
|
+
.ts-input {
|
|
676
|
+
position: relative;
|
|
677
|
+
display: flex;
|
|
678
|
+
align-items: center;
|
|
679
|
+
flex-wrap: wrap;
|
|
680
|
+
gap: 0.375rem;
|
|
681
|
+
width: 100%;
|
|
682
|
+
min-height: 42px;
|
|
683
|
+
padding: 0.5rem 0.75rem;
|
|
684
|
+
border: 1px solid #d9d9d9;
|
|
685
|
+
border-radius: 6px;
|
|
686
|
+
font-size: 0.875rem;
|
|
687
|
+
background-color: #ffffff;
|
|
688
|
+
cursor: pointer;
|
|
689
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
690
|
+
text-align: left;
|
|
691
|
+
}
|
|
692
|
+
.ts-input:hover { border-color: #4096ff; }
|
|
693
|
+
.ts-input:focus {
|
|
694
|
+
outline: none;
|
|
695
|
+
border-color: #1677ff;
|
|
696
|
+
box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
|
|
697
|
+
}
|
|
698
|
+
.ts-input:disabled {
|
|
699
|
+
background-color: #f5f5f5;
|
|
700
|
+
color: rgba(0, 0, 0, 0.25);
|
|
701
|
+
cursor: not-allowed;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.ts-input-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
705
|
+
.ts-placeholder { color: rgba(0, 0, 0, 0.45); }
|
|
706
|
+
.ts-arrow { color: rgba(0, 0, 0, 0.45); font-size: 0.75rem; transition: transform 0.2s; margin-left: auto; }
|
|
707
|
+
.ts-arrow-open { transform: rotate(180deg); }
|
|
708
|
+
.ts-clear {
|
|
709
|
+
padding: 0 0.25rem;
|
|
710
|
+
color: rgba(0, 0, 0, 0.45);
|
|
711
|
+
font-size: 1rem;
|
|
712
|
+
line-height: 1;
|
|
713
|
+
margin-left: auto;
|
|
714
|
+
}
|
|
715
|
+
.ts-clear:hover { color: rgba(0, 0, 0, 0.88); }
|
|
716
|
+
|
|
717
|
+
/* 触发器悬浮提示:展示全部已选项(maxTagCount 截断时补全) */
|
|
718
|
+
.ts-input[data-full]:not([data-full=''])::after {
|
|
719
|
+
content: attr(data-full);
|
|
720
|
+
position: absolute;
|
|
721
|
+
top: calc(100% + 8px);
|
|
722
|
+
left: 0;
|
|
723
|
+
padding: 0.5rem 0.75rem;
|
|
724
|
+
max-width: 18rem;
|
|
725
|
+
width: max-content;
|
|
726
|
+
white-space: normal;
|
|
727
|
+
word-break: break-all;
|
|
728
|
+
font-size: 0.75rem;
|
|
729
|
+
font-weight: 400;
|
|
730
|
+
text-align: left;
|
|
731
|
+
color: #fafafa;
|
|
732
|
+
background-color: rgba(0, 0, 0, 0.88);
|
|
733
|
+
border-radius: 6px;
|
|
734
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
735
|
+
opacity: 0;
|
|
736
|
+
visibility: hidden;
|
|
737
|
+
pointer-events: none;
|
|
738
|
+
transition: opacity 0.15s ease;
|
|
739
|
+
z-index: 100;
|
|
740
|
+
}
|
|
741
|
+
.ts-input[data-full]:not([data-full='']):not([aria-expanded='true']):hover::after {
|
|
742
|
+
opacity: 1;
|
|
743
|
+
visibility: visible;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.ts-tag {
|
|
747
|
+
display: inline-flex;
|
|
748
|
+
align-items: center;
|
|
749
|
+
padding: 1px 8px;
|
|
750
|
+
background: #f0f0f0;
|
|
751
|
+
border-radius: 4px;
|
|
752
|
+
font-size: 0.8125rem;
|
|
753
|
+
color: rgba(0, 0, 0, 0.88);
|
|
754
|
+
}
|
|
755
|
+
.ts-tag-more { color: rgba(0, 0, 0, 0.45); }
|
|
756
|
+
|
|
757
|
+
.ts-panel {
|
|
758
|
+
position: absolute;
|
|
759
|
+
z-index: 50;
|
|
760
|
+
top: calc(100% + 4px);
|
|
761
|
+
left: 0;
|
|
762
|
+
min-width: 240px;
|
|
763
|
+
max-height: 320px;
|
|
764
|
+
overflow: hidden;
|
|
765
|
+
display: flex;
|
|
766
|
+
flex-direction: column;
|
|
767
|
+
background: #ffffff;
|
|
768
|
+
border: 1px solid #d9d9d9;
|
|
769
|
+
border-radius: 8px;
|
|
770
|
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.ts-search { padding: 0.5rem; border-bottom: 1px solid #f0f0f0; }
|
|
774
|
+
.ts-search-input {
|
|
775
|
+
width: 100%;
|
|
776
|
+
padding: 0.375rem 0.5rem;
|
|
777
|
+
border: 1px solid #d9d9d9;
|
|
778
|
+
border-radius: 4px;
|
|
779
|
+
font-size: 0.875rem;
|
|
780
|
+
outline: none;
|
|
781
|
+
}
|
|
782
|
+
.ts-search-input:focus { border-color: #1677ff; }
|
|
783
|
+
|
|
784
|
+
.ts-toolbar {
|
|
785
|
+
display: flex;
|
|
786
|
+
gap: 0.5rem;
|
|
787
|
+
padding: 0.375rem 0.5rem;
|
|
788
|
+
border-bottom: 1px solid #f0f0f0;
|
|
789
|
+
}
|
|
790
|
+
.ts-tool-btn {
|
|
791
|
+
padding: 0.125rem 0.5rem;
|
|
792
|
+
border: 1px solid #d9d9d9;
|
|
793
|
+
border-radius: 4px;
|
|
794
|
+
background: transparent;
|
|
795
|
+
font-size: 0.75rem;
|
|
796
|
+
color: rgba(0, 0, 0, 0.65);
|
|
797
|
+
cursor: pointer;
|
|
798
|
+
transition: color 0.15s, border-color 0.15s;
|
|
799
|
+
}
|
|
800
|
+
.ts-tool-btn:hover { color: #1677ff; border-color: #1677ff; }
|
|
801
|
+
|
|
802
|
+
.ts-tree {
|
|
803
|
+
flex: 1;
|
|
804
|
+
overflow-y: auto;
|
|
805
|
+
padding: 0.375rem 0;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.ts-node {
|
|
809
|
+
display: flex;
|
|
810
|
+
align-items: center;
|
|
811
|
+
gap: 0.375rem;
|
|
812
|
+
padding: 0.375rem 0.5rem;
|
|
813
|
+
cursor: pointer;
|
|
814
|
+
font-size: 0.875rem;
|
|
815
|
+
color: rgba(0, 0, 0, 0.88);
|
|
816
|
+
border-radius: 4px;
|
|
817
|
+
margin: 0 0.25rem;
|
|
818
|
+
transition: background 0.15s;
|
|
819
|
+
}
|
|
820
|
+
.ts-node:hover { background: #f5f5f5; }
|
|
821
|
+
|
|
822
|
+
/* 树连接线(antd treeLine):横向连线 + 纵向缩进引导线 */
|
|
823
|
+
.ts-node.ts-tree-line { position: relative; }
|
|
824
|
+
.ts-node.ts-tree-line::before {
|
|
825
|
+
content: '';
|
|
826
|
+
position: absolute;
|
|
827
|
+
top: 0;
|
|
828
|
+
bottom: 0;
|
|
829
|
+
left: 8px;
|
|
830
|
+
border-left: 1px solid #d9d9d9;
|
|
831
|
+
}
|
|
832
|
+
.ts-node.ts-tree-line:first-child::before { top: 50%; }
|
|
833
|
+
.ts-node.ts-tree-line:last-child::before { bottom: 50%; }
|
|
834
|
+
.ts-node.ts-tree-line::after {
|
|
835
|
+
content: '';
|
|
836
|
+
position: absolute;
|
|
837
|
+
top: 50%;
|
|
838
|
+
left: 8px;
|
|
839
|
+
width: 12px;
|
|
840
|
+
border-top: 1px solid #d9d9d9;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.ts-checkbox, .ts-radio {
|
|
844
|
+
display: inline-flex;
|
|
845
|
+
align-items: center;
|
|
846
|
+
font-size: 0.9375rem;
|
|
847
|
+
color: #1677ff;
|
|
848
|
+
flex-shrink: 0;
|
|
849
|
+
width: 16px;
|
|
850
|
+
justify-content: center;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
.ts-node-arrow {
|
|
854
|
+
display: inline-flex;
|
|
855
|
+
align-items: center;
|
|
856
|
+
width: 16px;
|
|
857
|
+
font-size: 0.625rem;
|
|
858
|
+
color: rgba(0, 0, 0, 0.45);
|
|
859
|
+
transition: transform 0.2s;
|
|
860
|
+
flex-shrink: 0;
|
|
861
|
+
cursor: default;
|
|
862
|
+
}
|
|
863
|
+
.ts-node-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
864
|
+
.ts-node-leaf { display: inline-block; width: 16px; text-align: center; color: rgba(0, 0, 0, 0.25); font-size: 1rem; flex-shrink: 0; }
|
|
865
|
+
|
|
866
|
+
.ts-empty {
|
|
867
|
+
padding: 1rem;
|
|
868
|
+
text-align: center;
|
|
869
|
+
font-size: 0.875rem;
|
|
870
|
+
color: rgba(0, 0, 0, 0.45);
|
|
871
|
+
}
|
|
872
|
+
</style>
|