@opentiny/tiny-robot 0.1.0 → 0.2.0-alpha.1
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/dist/action-group/ActionGroup.vue.d.ts +26 -0
- package/dist/action-group/ActionGroupItem.vue.d.ts +18 -0
- package/dist/action-group/index.d.ts +12 -0
- package/dist/action-group/index.type.d.ts +16 -0
- package/dist/bubble/index.type.d.ts +1 -1
- package/dist/container/index.d.ts +7 -0
- package/dist/container/index.type.d.ts +16 -0
- package/dist/container/index.vue.d.ts +26 -0
- package/dist/feedback/components/SourceList.vue.d.ts +11 -0
- package/dist/feedback/components/index.d.ts +1 -0
- package/dist/feedback/index.d.ts +7 -0
- package/dist/feedback/index.type.d.ts +25 -0
- package/dist/feedback/index.vue.d.ts +13 -0
- package/dist/history/components/index.d.ts +2 -0
- package/dist/history/components/item-tag.vue.d.ts +5 -0
- package/dist/history/components/search-empty.vue.d.ts +7 -0
- package/dist/history/composables/index.d.ts +1 -0
- package/dist/history/composables/useEditItemTitle.d.ts +12 -0
- package/dist/history/index.d.ts +6 -0
- package/dist/history/index.type.d.ts +43 -0
- package/dist/history/index.vue.d.ts +2 -0
- package/dist/icon-button/index.d.ts +7 -0
- package/dist/icon-button/index.type.d.ts +7 -0
- package/dist/icon-button/index.vue.d.ts +6 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +56 -22
- package/dist/node_modules/.pnpm/@opentiny_utils@3.22.0/node_modules/@opentiny/utils/dist/index.es.js +1335 -884
- package/dist/node_modules/.pnpm/@opentiny_vue-common@3.22.0/node_modules/@opentiny/vue-common/lib/index.js +660 -144
- package/dist/node_modules/.pnpm/@opentiny_vue-hooks@3.22.0/node_modules/@opentiny/vue-hooks/dist/src/vue-popper.js +85 -0
- package/dist/node_modules/.pnpm/@opentiny_vue-locale@3.22.0/node_modules/@opentiny/vue-locale/lib/index.js +1783 -0
- package/dist/node_modules/.pnpm/@opentiny_vue-renderless@3.22.0/node_modules/@opentiny/vue-renderless/tooltip/index.js +77 -0
- package/dist/node_modules/.pnpm/@opentiny_vue-renderless@3.22.0/node_modules/@opentiny/vue-renderless/tooltip/vue.js +90 -0
- package/dist/node_modules/.pnpm/@opentiny_vue-tooltip@3.22.0/node_modules/@opentiny/vue-tooltip/lib/index.js +176 -0
- package/dist/node_modules/.pnpm/@opentiny_vue-tooltip@3.22.0/node_modules/@opentiny/vue-tooltip/lib/pc.js +248 -0
- package/dist/node_modules/.pnpm/@vueuse_core@13.1.0_vue@3.5.13/node_modules/@vueuse/core/index.js +190 -0
- package/dist/node_modules/.pnpm/@vueuse_shared@13.1.0_vue@3.5.13/node_modules/@vueuse/shared/index.js +53 -0
- package/dist/packages/components/src/action-group/ActionGroup.vue.js +7 -0
- package/dist/packages/components/src/action-group/ActionGroup.vue2.js +97 -0
- package/dist/packages/components/src/action-group/ActionGroupItem.vue.js +14 -0
- package/dist/packages/components/src/action-group/ActionGroupItem.vue2.js +4 -0
- package/dist/packages/components/src/action-group/index.js +17 -0
- package/dist/packages/components/src/bubble/bubble-list.vue.js +2 -2
- package/dist/packages/components/src/bubble/bubble-list.vue2.js +18 -19
- package/dist/packages/components/src/bubble/bubble.vue.js +2 -2
- package/dist/packages/components/src/bubble/bubble.vue2.js +46 -46
- package/dist/packages/components/src/container/index.js +9 -0
- package/dist/packages/components/src/container/index.vue.js +7 -0
- package/dist/packages/components/src/container/index.vue2.js +55 -0
- package/dist/packages/components/src/feedback/components/SourceList.vue.js +7 -0
- package/dist/packages/components/src/feedback/components/SourceList.vue2.js +52 -0
- package/dist/packages/components/src/feedback/index.js +9 -0
- package/dist/packages/components/src/feedback/index.vue.js +7 -0
- package/dist/packages/components/src/feedback/index.vue2.js +142 -0
- package/dist/packages/components/src/history/components/item-tag.vue.js +7 -0
- package/dist/packages/components/src/history/components/item-tag.vue2.js +21 -0
- package/dist/packages/components/src/history/components/search-empty.vue.js +7 -0
- package/dist/packages/components/src/history/components/search-empty.vue2.js +20 -0
- package/dist/packages/components/src/history/composables/useEditItemTitle.js +43 -0
- package/dist/packages/components/src/history/index.js +11 -0
- package/dist/packages/components/src/history/index.vue.js +7 -0
- package/dist/packages/components/src/history/index.vue2.js +130 -0
- package/dist/packages/components/src/icon-button/index.js +9 -0
- package/dist/packages/components/src/icon-button/index.vue.js +7 -0
- package/dist/packages/components/src/icon-button/index.vue2.js +40 -0
- package/dist/packages/components/src/prompts/prompt.vue.js +2 -2
- package/dist/packages/components/src/prompts/prompt.vue2.js +17 -15
- package/dist/packages/components/src/question/components/HotQuestions.vue.js +22 -22
- package/dist/packages/components/src/question/index.vue.js +7 -7
- package/dist/packages/components/src/sender/components/TemplateEditor.vue.js +7 -0
- package/dist/packages/components/src/sender/components/TemplateEditor.vue2.js +121 -0
- package/dist/packages/components/src/sender/index.vue.js +149 -128
- package/dist/packages/components/src/suggestion/components/CategoryNav.vue.js +38 -0
- package/dist/packages/components/src/suggestion/components/CategoryNav.vue2.js +4 -0
- package/dist/packages/components/src/suggestion/components/SuggestionCapsule.vue.js +107 -0
- package/dist/packages/components/src/suggestion/components/SuggestionCapsule.vue2.js +4 -0
- package/dist/packages/components/src/suggestion/components/SuggestionPanel.vue.js +123 -0
- package/dist/packages/components/src/suggestion/components/SuggestionPanel.vue2.js +4 -0
- package/dist/packages/components/src/suggestion/composables/useKeyboardNavigation.js +45 -0
- package/dist/packages/components/src/suggestion/composables/useTriggerDetection.js +17 -0
- package/dist/packages/components/src/suggestion/index.js +9 -0
- package/dist/packages/components/src/suggestion/index.vue.js +179 -0
- package/dist/packages/components/src/suggestion/index.vue2.js +4 -0
- package/dist/packages/components/src/suggestion/utils/dom.js +18 -0
- package/dist/packages/svgs/dist/tiny-robot-svgs.js +364 -69
- package/dist/question/components/HotQuestions.vue.d.ts +2 -2
- package/dist/question/index.vue.d.ts +1 -1
- package/dist/sender/components/ActionButtons.vue.d.ts +2 -2
- package/dist/sender/components/TemplateEditor.vue.d.ts +18 -0
- package/dist/sender/index.type.d.ts +47 -0
- package/dist/sender/index.vue.d.ts +70 -5
- package/dist/style.css +1 -1
- package/dist/suggestion/components/CategoryNav.vue.d.ts +45 -0
- package/dist/suggestion/components/SuggestionCapsule.vue.d.ts +32 -0
- package/dist/suggestion/components/SuggestionPanel.vue.d.ts +84 -0
- package/dist/suggestion/composables/useKeyboardNavigation.d.ts +18 -0
- package/dist/suggestion/composables/useSuggestionFilter.d.ts +10 -0
- package/dist/suggestion/composables/useTriggerDetection.d.ts +11 -0
- package/dist/suggestion/index.d.ts +7 -0
- package/dist/suggestion/index.type.d.ts +94 -0
- package/dist/suggestion/index.vue.d.ts +343 -0
- package/dist/suggestion/utils/dom.d.ts +20 -0
- package/package.json +5 -5
- package/src/action-group/ActionGroup.vue +232 -0
- package/src/action-group/ActionGroupItem.vue +9 -0
- package/src/action-group/index.ts +25 -0
- package/src/action-group/index.type.ts +20 -0
- package/src/bubble/bubble-list.vue +1 -3
- package/src/bubble/bubble.vue +4 -14
- package/src/bubble/index.type.ts +1 -1
- package/src/container/index.ts +12 -0
- package/src/container/index.type.ts +17 -0
- package/src/container/index.vue +134 -0
- package/src/feedback/components/SourceList.vue +112 -0
- package/src/feedback/components/index.ts +1 -0
- package/src/feedback/index.ts +12 -0
- package/src/feedback/index.type.ts +27 -0
- package/src/feedback/index.vue +166 -0
- package/src/history/components/index.ts +2 -0
- package/src/history/components/item-tag.vue +49 -0
- package/src/history/components/search-empty.vue +38 -0
- package/src/history/composables/index.ts +1 -0
- package/src/history/composables/useEditItemTitle.ts +75 -0
- package/src/history/index.ts +12 -0
- package/src/history/index.type.ts +50 -0
- package/src/history/index.vue +292 -0
- package/src/icon-button/index.ts +12 -0
- package/src/icon-button/index.type.ts +8 -0
- package/src/icon-button/index.vue +52 -0
- package/src/index.ts +37 -2
- package/src/prompts/prompt.vue +7 -21
- package/src/question/components/HotQuestions.vue +1 -1
- package/src/question/index.less +9 -10
- package/src/sender/components/TemplateEditor.vue +274 -0
- package/src/sender/index.less +17 -7
- package/src/sender/index.type.ts +51 -0
- package/src/sender/index.vue +56 -8
- package/src/sender/vars.less +3 -3
- package/src/suggestion/components/CategoryNav.vue +38 -0
- package/src/suggestion/components/SuggestionCapsule.vue +183 -0
- package/src/suggestion/components/SuggestionPanel.vue +147 -0
- package/src/suggestion/composables/useKeyboardNavigation.ts +101 -0
- package/src/suggestion/composables/useSuggestionFilter.ts +34 -0
- package/src/suggestion/composables/useTriggerDetection.ts +46 -0
- package/src/suggestion/index.less +497 -0
- package/src/suggestion/index.ts +12 -0
- package/src/suggestion/index.type.ts +101 -0
- package/src/suggestion/index.vue +338 -0
- package/src/suggestion/utils/dom.ts +66 -0
- package/src/suggestion/vars.less +141 -0
- package/.vscode/extensions.json +0 -3
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch, onMounted, onBeforeUnmount, nextTick, PropType, computed, watchEffect } from 'vue'
|
|
3
|
+
import type { SuggestionItem } from '../index.type'
|
|
4
|
+
import { IconEdit } from '@opentiny/tiny-robot-svgs'
|
|
5
|
+
import { getCachedTextWidth } from '../utils/dom'
|
|
6
|
+
|
|
7
|
+
const props = defineProps({
|
|
8
|
+
suggestions: {
|
|
9
|
+
type: Array as PropType<SuggestionItem[]>,
|
|
10
|
+
required: true,
|
|
11
|
+
},
|
|
12
|
+
isExpanded: {
|
|
13
|
+
type: Boolean,
|
|
14
|
+
default: false,
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits(['suggestion-click', 'show-expand-button'])
|
|
19
|
+
|
|
20
|
+
// eslint-disable-next-line
|
|
21
|
+
const capsuleRef = ref<any>(null)
|
|
22
|
+
const showExpandButton = ref(false)
|
|
23
|
+
const isExpandedRef = ref(false)
|
|
24
|
+
const visibleSuggestions = ref<SuggestionItem[]>([])
|
|
25
|
+
const hiddenSuggestions = ref<SuggestionItem[]>([])
|
|
26
|
+
const maxItemsPerRow = ref(0)
|
|
27
|
+
const resizeObserver = ref<ResizeObserver | null>(null)
|
|
28
|
+
|
|
29
|
+
// 计算每行的胶囊排列,实现从下到上、从左往右的排序
|
|
30
|
+
const arrangedHiddenSuggestions = computed(() => {
|
|
31
|
+
if (maxItemsPerRow.value <= 0 || hiddenSuggestions.value.length === 0) return []
|
|
32
|
+
|
|
33
|
+
// 创建行数组
|
|
34
|
+
const rows = []
|
|
35
|
+
const totalSuggestions = [...hiddenSuggestions.value]
|
|
36
|
+
|
|
37
|
+
// 按照每行最大容量分组
|
|
38
|
+
while (totalSuggestions.length > 0) {
|
|
39
|
+
rows.push(totalSuggestions.splice(0, maxItemsPerRow.value))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 返回行的数组,以便从下到上显示
|
|
43
|
+
return rows.reverse()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// 检查是否需要显示展开/收起按钮并计算能够容纳的最大胶囊数
|
|
47
|
+
const calculateLayout = async () => {
|
|
48
|
+
await nextTick()
|
|
49
|
+
if (!capsuleRef.value || props.suggestions.length === 0) {
|
|
50
|
+
showExpandButton.value = false
|
|
51
|
+
emit('show-expand-button', false)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const container = capsuleRef.value
|
|
56
|
+
const containerWidth = container.clientWidth
|
|
57
|
+
|
|
58
|
+
// 重置胶囊分组
|
|
59
|
+
visibleSuggestions.value = []
|
|
60
|
+
hiddenSuggestions.value = []
|
|
61
|
+
|
|
62
|
+
// 计算一行可以容纳多少个胶囊
|
|
63
|
+
let accumulatedWidth = 0
|
|
64
|
+
const margin = 8 // 胶囊之间的间距
|
|
65
|
+
|
|
66
|
+
// 先测量一个平均宽度来估算每行可容纳的胶囊数
|
|
67
|
+
let totalWidth = 0
|
|
68
|
+
const sampleSize = Math.min(3, props.suggestions.length)
|
|
69
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
70
|
+
// 使用缓存测量宽度
|
|
71
|
+
const itemWidth = getCachedTextWidth(props.suggestions[i].text, 'tr-common-suggestions_item') + margin
|
|
72
|
+
totalWidth += itemWidth
|
|
73
|
+
}
|
|
74
|
+
const avgItemWidth = totalWidth / sampleSize
|
|
75
|
+
maxItemsPerRow.value = Math.max(1, Math.floor(containerWidth / avgItemWidth))
|
|
76
|
+
|
|
77
|
+
// 计算每个胶囊的宽度并确定一行能容纳多少个
|
|
78
|
+
for (let i = 0; i < props.suggestions.length; i++) {
|
|
79
|
+
const suggestion = props.suggestions[i]
|
|
80
|
+
// 使用缓存测量宽度
|
|
81
|
+
const itemWidth = getCachedTextWidth(suggestion.text, 'tr-common-suggestions_item') + margin
|
|
82
|
+
|
|
83
|
+
// 检查是否还能容纳下一个胶囊
|
|
84
|
+
if (accumulatedWidth + itemWidth <= containerWidth) {
|
|
85
|
+
accumulatedWidth += itemWidth
|
|
86
|
+
visibleSuggestions.value.push(suggestion)
|
|
87
|
+
} else {
|
|
88
|
+
hiddenSuggestions.value.push(suggestion)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 如果有隐藏的胶囊,显示展开按钮
|
|
93
|
+
showExpandButton.value = hiddenSuggestions.value.length > 0
|
|
94
|
+
|
|
95
|
+
// 通知父组件是否需要显示展开按钮
|
|
96
|
+
emit('show-expand-button', showExpandButton.value)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 处理指令点击
|
|
100
|
+
const handleSuggestionClick = (suggestion: SuggestionItem) => {
|
|
101
|
+
emit('suggestion-click', suggestion)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 同步外部展开状态
|
|
105
|
+
watch(
|
|
106
|
+
() => props.isExpanded,
|
|
107
|
+
(val) => {
|
|
108
|
+
isExpandedRef.value = val
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
watchEffect(() => {
|
|
113
|
+
if (capsuleRef.value && props.suggestions.length) {
|
|
114
|
+
calculateLayout()
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// 组件挂载时使用ResizeObserver监听大小变化
|
|
119
|
+
onMounted(() => {
|
|
120
|
+
if (capsuleRef.value) {
|
|
121
|
+
resizeObserver.value = new ResizeObserver(() => {
|
|
122
|
+
calculateLayout()
|
|
123
|
+
})
|
|
124
|
+
resizeObserver.value.observe(capsuleRef.value)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// 组件卸载时移除监听器
|
|
129
|
+
onBeforeUnmount(() => {
|
|
130
|
+
if (resizeObserver.value) {
|
|
131
|
+
resizeObserver.value.disconnect()
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
</script>
|
|
135
|
+
|
|
136
|
+
<template>
|
|
137
|
+
<!-- 胶囊式指令 -->
|
|
138
|
+
<div class="tr-common-suggestions" ref="capsuleRef" :class="{ expanded: isExpandedRef }">
|
|
139
|
+
<div class="tr-common-suggestions_content">
|
|
140
|
+
<!-- 向上展开的隐藏胶囊 -->
|
|
141
|
+
<div class="tr-common-suggestions_expanded-wrapper">
|
|
142
|
+
<div v-show="isExpandedRef && hiddenSuggestions.length > 0" class="tr-common-suggestions_expanded-area">
|
|
143
|
+
<div
|
|
144
|
+
v-for="(row, rowIndex) in arrangedHiddenSuggestions"
|
|
145
|
+
:key="`row-${rowIndex}`"
|
|
146
|
+
class="tr-common-suggestions_row"
|
|
147
|
+
>
|
|
148
|
+
<div
|
|
149
|
+
v-for="(suggestion, index) in row"
|
|
150
|
+
:key="`hidden-${suggestion.id}-${index}`"
|
|
151
|
+
class="tr-common-suggestions_item"
|
|
152
|
+
@click="handleSuggestionClick(suggestion)"
|
|
153
|
+
>
|
|
154
|
+
<div class="tr-common-suggestions_item_icon">
|
|
155
|
+
<IconEdit />
|
|
156
|
+
</div>
|
|
157
|
+
<div class="tr-common-suggestions_item_text">
|
|
158
|
+
{{ suggestion.text }}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<!-- 固定在底部的可见胶囊 -->
|
|
166
|
+
<div class="tr-common-suggestions_container">
|
|
167
|
+
<div
|
|
168
|
+
v-for="(suggestion, index) in visibleSuggestions"
|
|
169
|
+
:key="`visible-${suggestion.id}-${index}`"
|
|
170
|
+
class="tr-common-suggestions_item"
|
|
171
|
+
@click="handleSuggestionClick(suggestion)"
|
|
172
|
+
>
|
|
173
|
+
<div class="tr-common-suggestions_item_icon">
|
|
174
|
+
<IconEdit />
|
|
175
|
+
</div>
|
|
176
|
+
<div class="tr-common-suggestions_item_text">
|
|
177
|
+
{{ suggestion.text }}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</template>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, PropType } from 'vue'
|
|
3
|
+
import { SuggestionItem, Category } from '../index.type'
|
|
4
|
+
import CategoryNav from './CategoryNav.vue'
|
|
5
|
+
import { useKeyboardNavigation } from '../composables/useKeyboardNavigation'
|
|
6
|
+
import { IconHotQuestion, IconClose } from '@opentiny/tiny-robot-svgs'
|
|
7
|
+
|
|
8
|
+
const props = defineProps({
|
|
9
|
+
items: {
|
|
10
|
+
type: Array as PropType<SuggestionItem[]>,
|
|
11
|
+
required: true,
|
|
12
|
+
},
|
|
13
|
+
categories: {
|
|
14
|
+
type: Array as PropType<Category[]>,
|
|
15
|
+
default: () => [],
|
|
16
|
+
},
|
|
17
|
+
loading: {
|
|
18
|
+
type: Boolean,
|
|
19
|
+
default: false,
|
|
20
|
+
},
|
|
21
|
+
title: {
|
|
22
|
+
type: String,
|
|
23
|
+
default: '指令',
|
|
24
|
+
},
|
|
25
|
+
maxVisibleItems: {
|
|
26
|
+
type: Number,
|
|
27
|
+
default: 5,
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const emit = defineEmits(['close', 'select', 'category-select', 'item-hover'])
|
|
32
|
+
|
|
33
|
+
const items = ref(props.items)
|
|
34
|
+
|
|
35
|
+
const { handleKeyDown, activeIndex } = useKeyboardNavigation(items, {
|
|
36
|
+
initialIndex: 0,
|
|
37
|
+
onClose: () => emit('close'),
|
|
38
|
+
onSelect: (item) => emit('select', item),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// 当前激活的分类
|
|
42
|
+
const activeCategory = ref<string>(props.categories.length > 0 ? props.categories[0].id : '')
|
|
43
|
+
|
|
44
|
+
// 计算当前激活分类的项目列表
|
|
45
|
+
const filteredItems = computed(() => {
|
|
46
|
+
// 如果没有分类或没有选中分类,直接显示 items
|
|
47
|
+
if (props.categories.length === 0 || !activeCategory.value) {
|
|
48
|
+
return props.items
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 查找当前激活的分类
|
|
52
|
+
const category = props.categories.find((cat) => cat.id === activeCategory.value)
|
|
53
|
+
return category ? category.items : []
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// 处理分类选择
|
|
57
|
+
const handleCategorySelect = (categoryId: string) => {
|
|
58
|
+
activeCategory.value = categoryId
|
|
59
|
+
|
|
60
|
+
const category = props.categories.find((cat) => cat.id === categoryId)
|
|
61
|
+
if (category) {
|
|
62
|
+
emit('category-select', category)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 处理选择指令项
|
|
67
|
+
const handleSelect = (item: SuggestionItem) => {
|
|
68
|
+
emit('select', item)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 处理关闭面板
|
|
72
|
+
const handleClose = () => {
|
|
73
|
+
emit('close')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 处理鼠标悬停
|
|
77
|
+
const handleItemHover = (index: number) => {
|
|
78
|
+
emit('item-hover', index)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
defineExpose({
|
|
82
|
+
handleKeyDown,
|
|
83
|
+
})
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<template>
|
|
87
|
+
<div class="tr-suggestion-panel">
|
|
88
|
+
<!-- 面板标题 -->
|
|
89
|
+
<div class="tr-suggestion-header">
|
|
90
|
+
<div class="tr-suggestion-header-left">
|
|
91
|
+
<div class="tr-suggestion-header-icon">
|
|
92
|
+
<slot name="title-icon">
|
|
93
|
+
<IconHotQuestion />
|
|
94
|
+
</slot>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="tr-suggestion-header-title">{{ title }}</div>
|
|
97
|
+
</div>
|
|
98
|
+
<span class="tr-suggestion-close-btn" @click="handleClose">
|
|
99
|
+
<span class="close-icon"><IconClose /></span>
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- 分类导航 -->
|
|
104
|
+
<CategoryNav
|
|
105
|
+
v-if="categories.length > 0"
|
|
106
|
+
:categories="categories"
|
|
107
|
+
:active-category="activeCategory"
|
|
108
|
+
@category-select="handleCategorySelect"
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
<!-- 内容区域 -->
|
|
112
|
+
<div
|
|
113
|
+
class="tr-suggestion-content"
|
|
114
|
+
:style="{ 'max-height': filteredItems.length > maxVisibleItems ? `${maxVisibleItems * 56}px` : 'auto' }"
|
|
115
|
+
>
|
|
116
|
+
<div v-if="loading" class="tr-suggestion-loading">
|
|
117
|
+
<slot name="loading-indicator">
|
|
118
|
+
<div class="tr-suggestion-loading-spinner"></div>
|
|
119
|
+
</slot>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<ul v-else-if="filteredItems.length > 0">
|
|
123
|
+
<li
|
|
124
|
+
v-for="(item, index) in filteredItems"
|
|
125
|
+
:key="item.id"
|
|
126
|
+
class="tr-suggestion-list-item"
|
|
127
|
+
:class="{ 'tr-suggestion-item-active': index === activeIndex }"
|
|
128
|
+
@click="handleSelect(item)"
|
|
129
|
+
@mouseenter="handleItemHover(index)"
|
|
130
|
+
>
|
|
131
|
+
<slot name="item" :item="item" :active="index === activeIndex">
|
|
132
|
+
<div class="item-content">
|
|
133
|
+
<div class="item-label">{{ item.text }}</div>
|
|
134
|
+
<div v-if="item.description" class="item-description">{{ item.description }}</div>
|
|
135
|
+
</div>
|
|
136
|
+
</slot>
|
|
137
|
+
</li>
|
|
138
|
+
</ul>
|
|
139
|
+
|
|
140
|
+
<div v-else class="tr-suggestion-empty">
|
|
141
|
+
<slot name="empty">
|
|
142
|
+
<p>无匹配结果</p>
|
|
143
|
+
</slot>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</template>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ref, Ref, watch } from 'vue'
|
|
2
|
+
import type { SuggestionItem } from '../index.type'
|
|
3
|
+
|
|
4
|
+
export interface KeyboardNavigationOptions {
|
|
5
|
+
onSelect?: (item: SuggestionItem) => void
|
|
6
|
+
onClose?: () => void
|
|
7
|
+
initialIndex?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 处理指令列表的键盘导航
|
|
12
|
+
* @param items 当前可见的指令项列表
|
|
13
|
+
* @param options 配置选项
|
|
14
|
+
*/
|
|
15
|
+
export function useKeyboardNavigation(items: Ref<SuggestionItem[]>, options: KeyboardNavigationOptions = {}) {
|
|
16
|
+
const activeIndex = ref(options.initialIndex || -1)
|
|
17
|
+
|
|
18
|
+
// 当列表变化时重置激活项
|
|
19
|
+
watch(items, (newItems) => {
|
|
20
|
+
if (newItems.length > 0 && activeIndex.value === -1) {
|
|
21
|
+
activeIndex.value = 0
|
|
22
|
+
} else if (newItems.length === 0) {
|
|
23
|
+
activeIndex.value = -1
|
|
24
|
+
} else if (activeIndex.value >= newItems.length) {
|
|
25
|
+
activeIndex.value = newItems.length - 1
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 处理键盘按键
|
|
31
|
+
* @param e 键盘事件
|
|
32
|
+
*/
|
|
33
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
34
|
+
if (items.value.length === 0) return
|
|
35
|
+
|
|
36
|
+
switch (e.key) {
|
|
37
|
+
case 'ArrowUp':
|
|
38
|
+
e.preventDefault()
|
|
39
|
+
activeIndex.value = (activeIndex.value - 1 + items.value.length) % items.value.length
|
|
40
|
+
scrollToActive()
|
|
41
|
+
break
|
|
42
|
+
|
|
43
|
+
case 'ArrowDown':
|
|
44
|
+
e.preventDefault()
|
|
45
|
+
activeIndex.value = (activeIndex.value + 1) % items.value.length
|
|
46
|
+
scrollToActive()
|
|
47
|
+
break
|
|
48
|
+
|
|
49
|
+
case 'Enter':
|
|
50
|
+
|
|
51
|
+
case 'Tab':
|
|
52
|
+
e.preventDefault()
|
|
53
|
+
if (activeIndex.value >= 0 && activeIndex.value < items.value.length) {
|
|
54
|
+
options.onSelect?.(items.value[activeIndex.value])
|
|
55
|
+
}
|
|
56
|
+
break
|
|
57
|
+
|
|
58
|
+
case 'Escape':
|
|
59
|
+
e.preventDefault()
|
|
60
|
+
options.onClose?.()
|
|
61
|
+
break
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 滚动到当前激活项
|
|
67
|
+
*/
|
|
68
|
+
const scrollToActive = () => {
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
const activeElement = document.querySelector(`.tr-suggestion-list-item:nth-child(${activeIndex.value + 1})`)
|
|
71
|
+
const container = activeElement?.closest('.tr-suggestion-content')
|
|
72
|
+
|
|
73
|
+
if (activeElement && container) {
|
|
74
|
+
const containerRect = container.getBoundingClientRect()
|
|
75
|
+
const itemRect = activeElement.getBoundingClientRect()
|
|
76
|
+
|
|
77
|
+
if (itemRect.bottom > containerRect.bottom) {
|
|
78
|
+
container.scrollTop += itemRect.bottom - containerRect.bottom
|
|
79
|
+
} else if (itemRect.top < containerRect.top) {
|
|
80
|
+
container.scrollTop -= containerRect.top - itemRect.top
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}, 0)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 设置激活项索引
|
|
88
|
+
*/
|
|
89
|
+
const setActiveIndex = (index: number) => {
|
|
90
|
+
if (index >= -1 && index < items.value.length) {
|
|
91
|
+
activeIndex.value = index
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
activeIndex,
|
|
97
|
+
handleKeyDown,
|
|
98
|
+
scrollToActive,
|
|
99
|
+
setActiveIndex,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { computed, Ref } from 'vue'
|
|
2
|
+
import type { SuggestionItem } from '../index.type'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 过滤和搜索指令项列表
|
|
6
|
+
* @param items 原始指令项列表
|
|
7
|
+
* @param searchTerm 搜索关键词
|
|
8
|
+
*/
|
|
9
|
+
export function useSuggestionFilter(items: Ref<SuggestionItem[]>, searchTerm: Ref<string>) {
|
|
10
|
+
/**
|
|
11
|
+
* 根据搜索词过滤指令列表
|
|
12
|
+
*/
|
|
13
|
+
const filteredItems = computed(() => {
|
|
14
|
+
const term = searchTerm.value.trim().toLowerCase()
|
|
15
|
+
if (!term) return items.value
|
|
16
|
+
|
|
17
|
+
return items.value.filter((item) => {
|
|
18
|
+
// 搜索文本匹配
|
|
19
|
+
if (item.text.toLowerCase().includes(term)) return true
|
|
20
|
+
|
|
21
|
+
// 搜索描述匹配
|
|
22
|
+
if (item.description?.toLowerCase().includes(term)) return true
|
|
23
|
+
|
|
24
|
+
// 搜索关键词匹配
|
|
25
|
+
if (item.keywords?.some((keyword) => keyword.toLowerCase().includes(term))) return true
|
|
26
|
+
|
|
27
|
+
return false
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
filteredItems,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { SuggestionProps } from '../index.type'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 使用hook处理快捷键触发和命令过滤逻辑
|
|
5
|
+
*/
|
|
6
|
+
export const useTriggerDetection = (props: SuggestionProps) => {
|
|
7
|
+
/**
|
|
8
|
+
* 检测是否为触发快捷键
|
|
9
|
+
* @param char 当前字符
|
|
10
|
+
* @returns 是否为触发字符
|
|
11
|
+
*/
|
|
12
|
+
const isTriggerKey = (char: string) => {
|
|
13
|
+
return props.triggerKeys!.includes(char)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 处理输入事件,检测触发条件
|
|
18
|
+
* @param event 输入事件
|
|
19
|
+
* @param text 当前输入文本
|
|
20
|
+
* @returns 触发信息
|
|
21
|
+
*/
|
|
22
|
+
const detectTrigger = (event: Event, text: string) => {
|
|
23
|
+
const input = event.target as HTMLInputElement
|
|
24
|
+
const cursorPos = input.selectionStart || 0
|
|
25
|
+
|
|
26
|
+
// 检测是否输入了触发字符
|
|
27
|
+
if (
|
|
28
|
+
cursorPos > 0 &&
|
|
29
|
+
isTriggerKey(text[cursorPos - 1]) &&
|
|
30
|
+
(cursorPos === 1 || text[cursorPos - 2] === ' ' || text[cursorPos - 2] === '\n')
|
|
31
|
+
) {
|
|
32
|
+
// 设置触发信息
|
|
33
|
+
return {
|
|
34
|
+
text: text[cursorPos - 1],
|
|
35
|
+
position: cursorPos - 1,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
isTriggerKey,
|
|
44
|
+
detectTrigger,
|
|
45
|
+
}
|
|
46
|
+
}
|