@opentiny/tiny-robot 0.2.0-alpha.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.
Files changed (136) hide show
  1. package/dist/action-group/ActionGroup.vue.d.ts +26 -0
  2. package/dist/action-group/ActionGroupItem.vue.d.ts +18 -0
  3. package/dist/action-group/index.d.ts +12 -0
  4. package/dist/action-group/index.type.d.ts +16 -0
  5. package/dist/feedback/components/SourceList.vue.d.ts +11 -0
  6. package/dist/feedback/components/index.d.ts +1 -0
  7. package/dist/feedback/index.d.ts +7 -0
  8. package/dist/feedback/index.type.d.ts +25 -0
  9. package/dist/feedback/index.vue.d.ts +13 -0
  10. package/dist/history/components/index.d.ts +2 -0
  11. package/dist/history/components/item-tag.vue.d.ts +5 -0
  12. package/dist/history/components/search-empty.vue.d.ts +7 -0
  13. package/dist/history/composables/index.d.ts +1 -0
  14. package/dist/history/composables/useEditItemTitle.d.ts +12 -0
  15. package/dist/history/index.d.ts +6 -0
  16. package/dist/history/index.type.d.ts +43 -0
  17. package/dist/history/index.vue.d.ts +2 -0
  18. package/dist/icon-button/index.d.ts +7 -0
  19. package/dist/icon-button/index.type.d.ts +7 -0
  20. package/dist/icon-button/index.vue.d.ts +6 -0
  21. package/dist/index.d.ts +10 -1
  22. package/dist/index.js +57 -27
  23. package/dist/node_modules/.pnpm/@opentiny_utils@3.22.0/node_modules/@opentiny/utils/dist/index.es.js +1335 -884
  24. package/dist/node_modules/.pnpm/@opentiny_vue-common@3.22.0/node_modules/@opentiny/vue-common/lib/index.js +660 -144
  25. package/dist/node_modules/.pnpm/@opentiny_vue-hooks@3.22.0/node_modules/@opentiny/vue-hooks/dist/src/vue-popper.js +85 -0
  26. package/dist/node_modules/.pnpm/@opentiny_vue-locale@3.22.0/node_modules/@opentiny/vue-locale/lib/index.js +1783 -0
  27. package/dist/node_modules/.pnpm/@opentiny_vue-renderless@3.22.0/node_modules/@opentiny/vue-renderless/tooltip/index.js +77 -0
  28. package/dist/node_modules/.pnpm/@opentiny_vue-renderless@3.22.0/node_modules/@opentiny/vue-renderless/tooltip/vue.js +90 -0
  29. package/dist/node_modules/.pnpm/@opentiny_vue-tooltip@3.22.0/node_modules/@opentiny/vue-tooltip/lib/index.js +176 -0
  30. package/dist/node_modules/.pnpm/@opentiny_vue-tooltip@3.22.0/node_modules/@opentiny/vue-tooltip/lib/pc.js +248 -0
  31. package/dist/node_modules/.pnpm/@vueuse_core@13.1.0_vue@3.5.13/node_modules/@vueuse/core/index.js +190 -0
  32. package/dist/node_modules/.pnpm/@vueuse_shared@13.1.0_vue@3.5.13/node_modules/@vueuse/shared/index.js +53 -0
  33. package/dist/packages/components/src/action-group/ActionGroup.vue.js +7 -0
  34. package/dist/packages/components/src/action-group/ActionGroup.vue2.js +97 -0
  35. package/dist/packages/components/src/action-group/ActionGroupItem.vue.js +14 -0
  36. package/dist/packages/components/src/action-group/ActionGroupItem.vue2.js +4 -0
  37. package/dist/packages/components/src/action-group/index.js +17 -0
  38. package/dist/packages/components/src/bubble/bubble.vue.js +2 -2
  39. package/dist/packages/components/src/bubble/bubble.vue2.js +46 -46
  40. package/dist/packages/components/src/container/index.vue.js +1 -1
  41. package/dist/packages/components/src/container/index.vue2.js +17 -17
  42. package/dist/packages/components/src/feedback/components/SourceList.vue.js +7 -0
  43. package/dist/packages/components/src/feedback/components/SourceList.vue2.js +52 -0
  44. package/dist/packages/components/src/feedback/index.js +9 -0
  45. package/dist/packages/components/src/feedback/index.vue.js +7 -0
  46. package/dist/packages/components/src/feedback/index.vue2.js +142 -0
  47. package/dist/packages/components/src/history/components/item-tag.vue.js +7 -0
  48. package/dist/packages/components/src/history/components/item-tag.vue2.js +21 -0
  49. package/dist/packages/components/src/history/components/search-empty.vue.js +7 -0
  50. package/dist/packages/components/src/history/components/search-empty.vue2.js +20 -0
  51. package/dist/packages/components/src/history/composables/useEditItemTitle.js +43 -0
  52. package/dist/packages/components/src/history/index.js +11 -0
  53. package/dist/packages/components/src/history/index.vue.js +7 -0
  54. package/dist/packages/components/src/history/index.vue2.js +130 -0
  55. package/dist/packages/components/src/icon-button/index.js +9 -0
  56. package/dist/packages/components/src/icon-button/index.vue.js +7 -0
  57. package/dist/packages/components/src/icon-button/index.vue2.js +40 -0
  58. package/dist/packages/components/src/prompts/prompt.vue.js +2 -2
  59. package/dist/packages/components/src/prompts/prompt.vue2.js +17 -15
  60. package/dist/packages/components/src/question/components/HotQuestions.vue.js +23 -23
  61. package/dist/packages/components/src/question/index.vue.js +18 -18
  62. package/dist/packages/components/src/sender/components/TemplateEditor.vue.js +7 -0
  63. package/dist/packages/components/src/sender/components/TemplateEditor.vue2.js +121 -0
  64. package/dist/packages/components/src/sender/index.vue.js +149 -128
  65. package/dist/packages/components/src/suggestion/components/CategoryNav.vue.js +38 -0
  66. package/dist/packages/components/src/suggestion/components/CategoryNav.vue2.js +4 -0
  67. package/dist/packages/components/src/suggestion/components/SuggestionCapsule.vue.js +107 -0
  68. package/dist/packages/components/src/suggestion/components/SuggestionCapsule.vue2.js +4 -0
  69. package/dist/packages/components/src/suggestion/components/SuggestionPanel.vue.js +123 -0
  70. package/dist/packages/components/src/suggestion/components/SuggestionPanel.vue2.js +4 -0
  71. package/dist/packages/components/src/suggestion/composables/useKeyboardNavigation.js +45 -0
  72. package/dist/packages/components/src/suggestion/composables/useTriggerDetection.js +17 -0
  73. package/dist/packages/components/src/suggestion/index.js +9 -0
  74. package/dist/packages/components/src/suggestion/index.vue.js +179 -0
  75. package/dist/packages/components/src/suggestion/index.vue2.js +4 -0
  76. package/dist/packages/components/src/suggestion/utils/dom.js +18 -0
  77. package/dist/packages/svgs/dist/tiny-robot-svgs.js +306 -90
  78. package/dist/question/components/HotQuestions.vue.d.ts +2 -2
  79. package/dist/sender/components/TemplateEditor.vue.d.ts +18 -0
  80. package/dist/sender/index.type.d.ts +47 -0
  81. package/dist/sender/index.vue.d.ts +68 -3
  82. package/dist/style.css +1 -1
  83. package/dist/suggestion/components/CategoryNav.vue.d.ts +45 -0
  84. package/dist/suggestion/components/SuggestionCapsule.vue.d.ts +32 -0
  85. package/dist/suggestion/components/SuggestionPanel.vue.d.ts +84 -0
  86. package/dist/suggestion/composables/useKeyboardNavigation.d.ts +18 -0
  87. package/dist/suggestion/composables/useSuggestionFilter.d.ts +10 -0
  88. package/dist/suggestion/composables/useTriggerDetection.d.ts +11 -0
  89. package/dist/suggestion/index.d.ts +7 -0
  90. package/dist/suggestion/index.type.d.ts +94 -0
  91. package/dist/suggestion/index.vue.d.ts +343 -0
  92. package/dist/suggestion/utils/dom.d.ts +20 -0
  93. package/package.json +4 -3
  94. package/src/action-group/ActionGroup.vue +232 -0
  95. package/src/action-group/ActionGroupItem.vue +9 -0
  96. package/src/action-group/index.ts +25 -0
  97. package/src/action-group/index.type.ts +20 -0
  98. package/src/bubble/bubble.vue +4 -14
  99. package/src/container/index.vue +1 -2
  100. package/src/feedback/components/SourceList.vue +112 -0
  101. package/src/feedback/components/index.ts +1 -0
  102. package/src/feedback/index.ts +12 -0
  103. package/src/feedback/index.type.ts +27 -0
  104. package/src/feedback/index.vue +166 -0
  105. package/src/history/components/index.ts +2 -0
  106. package/src/history/components/item-tag.vue +49 -0
  107. package/src/history/components/search-empty.vue +38 -0
  108. package/src/history/composables/index.ts +1 -0
  109. package/src/history/composables/useEditItemTitle.ts +75 -0
  110. package/src/history/index.ts +12 -0
  111. package/src/history/index.type.ts +50 -0
  112. package/src/history/index.vue +292 -0
  113. package/src/icon-button/index.ts +12 -0
  114. package/src/icon-button/index.type.ts +8 -0
  115. package/src/icon-button/index.vue +52 -0
  116. package/src/index.ts +33 -1
  117. package/src/prompts/prompt.vue +7 -21
  118. package/src/question/components/HotQuestions.vue +1 -1
  119. package/src/question/index.less +9 -10
  120. package/src/sender/components/TemplateEditor.vue +274 -0
  121. package/src/sender/index.less +17 -7
  122. package/src/sender/index.type.ts +51 -0
  123. package/src/sender/index.vue +56 -8
  124. package/src/sender/vars.less +3 -3
  125. package/src/suggestion/components/CategoryNav.vue +38 -0
  126. package/src/suggestion/components/SuggestionCapsule.vue +183 -0
  127. package/src/suggestion/components/SuggestionPanel.vue +147 -0
  128. package/src/suggestion/composables/useKeyboardNavigation.ts +101 -0
  129. package/src/suggestion/composables/useSuggestionFilter.ts +34 -0
  130. package/src/suggestion/composables/useTriggerDetection.ts +46 -0
  131. package/src/suggestion/index.less +497 -0
  132. package/src/suggestion/index.ts +12 -0
  133. package/src/suggestion/index.type.ts +101 -0
  134. package/src/suggestion/index.vue +338 -0
  135. package/src/suggestion/utils/dom.ts +66 -0
  136. package/src/suggestion/vars.less +141 -0
@@ -0,0 +1,38 @@
1
+ <script setup lang="ts">
2
+ import { PropType } from 'vue'
3
+ import { Category } from '../index.type'
4
+
5
+ defineProps({
6
+ categories: {
7
+ type: Array as PropType<Category[]>,
8
+ required: true,
9
+ },
10
+ activeCategory: {
11
+ type: String,
12
+ default: '',
13
+ },
14
+ })
15
+
16
+ const emit = defineEmits(['category-select'])
17
+
18
+ const handleCategorySelect = (categoryId: string) => {
19
+ emit('category-select', categoryId)
20
+ }
21
+ </script>
22
+
23
+ <template>
24
+ <div class="tr-suggestion-categories">
25
+ <div
26
+ v-for="category in categories"
27
+ :key="category.id"
28
+ class="tr-suggestion-categories-item"
29
+ :class="{ active: activeCategory === category.id }"
30
+ @click="handleCategorySelect(category.id)"
31
+ >
32
+ <slot name="category-label" :category="category">
33
+ <div class="category-icon" v-if="category.icon">{{ category.icon }}</div>
34
+ <span>{{ category.label }}</span>
35
+ </slot>
36
+ </div>
37
+ </div>
38
+ </template>
@@ -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
+ }