@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,274 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, nextTick, watch } from 'vue'
|
|
3
|
+
import { TemplatePart, TemplateEditorProps, TemplateEditorEmits, TemplateEditorExpose } from '../index.type'
|
|
4
|
+
|
|
5
|
+
// 使用类型定义props
|
|
6
|
+
const props = defineProps<TemplateEditorProps>()
|
|
7
|
+
|
|
8
|
+
// 使用类型定义emits
|
|
9
|
+
const emit = defineEmits<TemplateEditorEmits>()
|
|
10
|
+
|
|
11
|
+
// 编辑器DOM引用
|
|
12
|
+
const editorRef = ref<HTMLElement | null>(null)
|
|
13
|
+
|
|
14
|
+
// 输入框引用集合
|
|
15
|
+
// eslint-disable-next-line
|
|
16
|
+
const inputRefs = ref<any>({})
|
|
17
|
+
|
|
18
|
+
// 当前激活的输入块索引
|
|
19
|
+
const activeFieldIndex = ref<number>(-1)
|
|
20
|
+
|
|
21
|
+
// 解析模板,将其分解为普通文本和可编辑字段
|
|
22
|
+
const templateParts = computed<TemplatePart[]>(() => {
|
|
23
|
+
// TODO: 如果已有值,使用现有值填充模板
|
|
24
|
+
if (props.value) {
|
|
25
|
+
return parseValueIntoTemplate()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 否则使用模板解析
|
|
29
|
+
const parts: TemplatePart[] = []
|
|
30
|
+
let currentIndex = 0
|
|
31
|
+
let fieldIndex = 0
|
|
32
|
+
|
|
33
|
+
// 正则表达式匹配 [xxx] 格式
|
|
34
|
+
const regex = /\[(.*?)\]/g
|
|
35
|
+
let match
|
|
36
|
+
|
|
37
|
+
while ((match = regex.exec(props.template)) !== null) {
|
|
38
|
+
// 添加匹配前的普通文本
|
|
39
|
+
if (match.index > currentIndex) {
|
|
40
|
+
parts.push({
|
|
41
|
+
content: props.template.substring(currentIndex, match.index),
|
|
42
|
+
isField: false,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 添加匹配的字段
|
|
47
|
+
parts.push({
|
|
48
|
+
content: '',
|
|
49
|
+
placeholder: match[1],
|
|
50
|
+
isField: true,
|
|
51
|
+
fieldIndex: fieldIndex++,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
currentIndex = match.index + match[0].length
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 添加剩余的普通文本
|
|
58
|
+
if (currentIndex < props.template.length) {
|
|
59
|
+
parts.push({
|
|
60
|
+
content: props.template.substring(currentIndex),
|
|
61
|
+
isField: false,
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return parts
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// 解析现有值填充到模板中
|
|
69
|
+
const parseValueIntoTemplate = (): TemplatePart[] => {
|
|
70
|
+
// 在实际应用中,这里应该根据模板和现有值进行匹配
|
|
71
|
+
return templateParts.value
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 生成完整的文本值
|
|
75
|
+
const generateValue = (): string => {
|
|
76
|
+
return templateParts.value.map((part) => part.content).join('')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 更新值并触发事件
|
|
80
|
+
const updateValue = (): void => {
|
|
81
|
+
const newValue = generateValue()
|
|
82
|
+
emit('update:value', newValue)
|
|
83
|
+
emit('input', newValue)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 激活输入块
|
|
87
|
+
const activateField = (index: number): void => {
|
|
88
|
+
activeFieldIndex.value = index
|
|
89
|
+
|
|
90
|
+
// 确保DOM更新后再聚焦
|
|
91
|
+
nextTick(() => {
|
|
92
|
+
if (inputRefs.value[index]) {
|
|
93
|
+
inputRefs.value[index]?.focus()
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 取消激活输入块
|
|
99
|
+
const deactivateField = (): void => {
|
|
100
|
+
updateValue()
|
|
101
|
+
activeFieldIndex.value = -1
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 处理输入框键盘事件
|
|
105
|
+
const handleInputKeyDown = (event: KeyboardEvent, index: number): void => {
|
|
106
|
+
if (event.key === 'Tab') {
|
|
107
|
+
event.preventDefault()
|
|
108
|
+
|
|
109
|
+
// 找到下一个输入块
|
|
110
|
+
const nextFieldIndex = findNextFieldIndex(index, event.shiftKey)
|
|
111
|
+
if (nextFieldIndex !== -1) {
|
|
112
|
+
// 先更新当前值
|
|
113
|
+
updateValue()
|
|
114
|
+
// 再激活下一个字段
|
|
115
|
+
activateField(nextFieldIndex)
|
|
116
|
+
}
|
|
117
|
+
} else if (event.key === 'Enter') {
|
|
118
|
+
event.preventDefault()
|
|
119
|
+
deactivateField()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 寻找下一个输入块索引,支持通过 shift+tab 反向查找
|
|
124
|
+
const findNextFieldIndex = (currentIndex: number, isReverse = false): number => {
|
|
125
|
+
if (isReverse) {
|
|
126
|
+
// 向前查找(Shift+Tab)
|
|
127
|
+
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
128
|
+
if (templateParts.value[i].isField) {
|
|
129
|
+
return i
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 如果没有找到,则循环到最后一个字段
|
|
134
|
+
for (let i = templateParts.value.length - 1; i > currentIndex; i--) {
|
|
135
|
+
if (templateParts.value[i].isField) {
|
|
136
|
+
return i
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
// 向后查找(Tab)
|
|
141
|
+
for (let i = currentIndex + 1; i < templateParts.value.length; i++) {
|
|
142
|
+
if (templateParts.value[i].isField) {
|
|
143
|
+
return i
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 循环回到第一个输入块
|
|
148
|
+
for (let i = 0; i < currentIndex; i++) {
|
|
149
|
+
if (templateParts.value[i].isField) {
|
|
150
|
+
return i
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return -1
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 监听值变化
|
|
159
|
+
watch(
|
|
160
|
+
() => props.value,
|
|
161
|
+
(newValue) => {
|
|
162
|
+
// 如果外部值为空字符串,清空所有字段内容
|
|
163
|
+
if (newValue === '') {
|
|
164
|
+
resetFields()
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
// 重置所有字段内容
|
|
170
|
+
const resetFields = (): void => {
|
|
171
|
+
// 清空所有输入块内容
|
|
172
|
+
templateParts.value.forEach((part) => {
|
|
173
|
+
if (part.isField) {
|
|
174
|
+
part.content = ''
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
// 更新值
|
|
178
|
+
updateValue()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 导出方法供父组件调用
|
|
182
|
+
defineExpose<TemplateEditorExpose>({
|
|
183
|
+
activateFirstField: () => {
|
|
184
|
+
for (let i = 0; i < templateParts.value.length; i++) {
|
|
185
|
+
if (templateParts.value[i].isField) {
|
|
186
|
+
activateField(i)
|
|
187
|
+
break
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
resetFields,
|
|
192
|
+
})
|
|
193
|
+
</script>
|
|
194
|
+
|
|
195
|
+
<template>
|
|
196
|
+
<div class="template-editor">
|
|
197
|
+
<div class="template-content" ref="editorRef">
|
|
198
|
+
<template v-for="(part, index) in templateParts" :key="index">
|
|
199
|
+
<!-- 普通文本部分 -->
|
|
200
|
+
<span v-if="!part.isField">{{ part.content }}</span>
|
|
201
|
+
|
|
202
|
+
<!-- 可编辑的输入块部分 -->
|
|
203
|
+
<span
|
|
204
|
+
v-else
|
|
205
|
+
class="template-field"
|
|
206
|
+
:class="{ 'template-field-active': activeFieldIndex === index }"
|
|
207
|
+
@click="activateField(index)"
|
|
208
|
+
>
|
|
209
|
+
<input
|
|
210
|
+
v-if="activeFieldIndex === index"
|
|
211
|
+
:ref="(el) => (inputRefs[index] = el)"
|
|
212
|
+
v-model="part.content"
|
|
213
|
+
class="template-input"
|
|
214
|
+
@blur="deactivateField()"
|
|
215
|
+
@keydown="handleInputKeyDown($event, index)"
|
|
216
|
+
@click.stop
|
|
217
|
+
/>
|
|
218
|
+
<span v-else class="template-placeholder">{{ part.content || part.placeholder }}</span>
|
|
219
|
+
</span>
|
|
220
|
+
</template>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</template>
|
|
224
|
+
|
|
225
|
+
<style scoped>
|
|
226
|
+
.template-editor {
|
|
227
|
+
width: 100%;
|
|
228
|
+
min-height: 26px;
|
|
229
|
+
line-height: 26px;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.template-content {
|
|
233
|
+
display: inline;
|
|
234
|
+
word-break: break-word;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.template-field {
|
|
238
|
+
display: inline-block;
|
|
239
|
+
height: 26px;
|
|
240
|
+
min-width: fit-content;
|
|
241
|
+
border-radius: 4px;
|
|
242
|
+
background: rgba(0, 0, 0, 0.05);
|
|
243
|
+
padding: 0 10px;
|
|
244
|
+
margin: 0 2px;
|
|
245
|
+
vertical-align: middle;
|
|
246
|
+
cursor: text;
|
|
247
|
+
transition: all 0.2s ease;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.template-field:hover {
|
|
251
|
+
background: rgba(0, 0, 0, 0.08);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.template-placeholder {
|
|
255
|
+
color: #666;
|
|
256
|
+
user-select: none;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.template-input {
|
|
260
|
+
box-sizing: border-box;
|
|
261
|
+
width: 100%;
|
|
262
|
+
height: 26px;
|
|
263
|
+
border: 1px solid rgb(194, 194, 194);
|
|
264
|
+
border-radius: 4px;
|
|
265
|
+
padding: 0 10px;
|
|
266
|
+
outline: none;
|
|
267
|
+
background: white;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.template-field-active {
|
|
271
|
+
background: transparent;
|
|
272
|
+
padding: 0;
|
|
273
|
+
}
|
|
274
|
+
</style>
|
package/src/sender/index.less
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
// 主要组件样式
|
|
4
4
|
.tiny-sender {
|
|
5
|
-
background: var(--tr-sender-bg-color);
|
|
6
5
|
position: relative;
|
|
7
6
|
color: var(--tr-sender-text-color);
|
|
8
7
|
|
|
@@ -31,6 +30,12 @@
|
|
|
31
30
|
color: var(--tr-sender-text-color-secondary);
|
|
32
31
|
}
|
|
33
32
|
|
|
33
|
+
.tiny-textarea.is-disabled .tiny-textarea__inner {
|
|
34
|
+
border: none;
|
|
35
|
+
background: var(--tr-sender-bg-color);
|
|
36
|
+
color: var(--tr-sender-text-color-secondary);
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
.tiny-input__suffix {
|
|
35
40
|
right: 0;
|
|
36
41
|
display: flex;
|
|
@@ -39,9 +44,10 @@
|
|
|
39
44
|
|
|
40
45
|
.tiny-textarea__inner {
|
|
41
46
|
border: none;
|
|
42
|
-
height:
|
|
43
|
-
|
|
44
|
-
padding
|
|
47
|
+
height: 26px;
|
|
48
|
+
|
|
49
|
+
padding: 0;
|
|
50
|
+
align-content: center; // 使内容垂直居中
|
|
45
51
|
background-color: var(--tr-sender-bg-color);
|
|
46
52
|
color: var(--tr-sender-text-color);
|
|
47
53
|
|
|
@@ -88,7 +94,6 @@
|
|
|
88
94
|
width: 100%;
|
|
89
95
|
border-radius: 0 0 var(--tr-sender-border-radius) var(--tr-sender-border-radius);
|
|
90
96
|
background: var(--tr-sender-footer-bg);
|
|
91
|
-
margin-top: var(--tr-sender-gap);
|
|
92
97
|
z-index: 1;
|
|
93
98
|
|
|
94
99
|
&.tiny-sender__bottom-row {
|
|
@@ -248,6 +253,11 @@
|
|
|
248
253
|
cursor: wait;
|
|
249
254
|
background-color: var(--tr-sender-bg-color);
|
|
250
255
|
}
|
|
256
|
+
|
|
257
|
+
.tiny-textarea__inner {
|
|
258
|
+
cursor: wait;
|
|
259
|
+
background-color: var(--tr-sender-bg-color);
|
|
260
|
+
}
|
|
251
261
|
}
|
|
252
262
|
|
|
253
263
|
// 错误状态
|
|
@@ -283,8 +293,8 @@
|
|
|
283
293
|
.action-buttons {
|
|
284
294
|
display: flex;
|
|
285
295
|
gap: var(--tr-sender-gap);
|
|
286
|
-
padding: var(--tr-sender-padding-
|
|
287
|
-
|
|
296
|
+
padding-left: var(--tr-sender-padding-left);
|
|
297
|
+
padding-right: var(--tr-sender-padding-right);
|
|
288
298
|
background: var(--tr-sender-bg-color);
|
|
289
299
|
border-radius: var(--tr-sender-border-radius);
|
|
290
300
|
align-items: center;
|
package/src/sender/index.type.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface SenderProps {
|
|
|
41
41
|
showWordLimit?: boolean // 显示字数统计
|
|
42
42
|
suggestions?: string[] // 输入建议
|
|
43
43
|
theme?: ThemeType // 主题
|
|
44
|
+
template?: string // 模板字符串,格式如 "你好 [称呼],感谢您的 [事项]"
|
|
45
|
+
hasContent?: boolean // 手动指定是否有内容,用于模板模式
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
export interface ActionButtonsProps {
|
|
@@ -72,6 +74,7 @@ export type SenderEmits = {
|
|
|
72
74
|
(e: 'blur', event: FocusEvent): void
|
|
73
75
|
(e: 'escape-press'): void // 按下Esc键时触发
|
|
74
76
|
(e: 'cancel'): void // 取消发送状态时触发
|
|
77
|
+
(e: 'reset-template'): void // 重置模板状态,退出模板编辑模式
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
// 语音识别状态
|
|
@@ -109,3 +112,51 @@ export interface SpeechHandler {
|
|
|
109
112
|
start: () => void
|
|
110
113
|
stop: () => void
|
|
111
114
|
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 模板部分定义
|
|
118
|
+
*/
|
|
119
|
+
export interface TemplatePart {
|
|
120
|
+
/** 内容文本 */
|
|
121
|
+
content: string
|
|
122
|
+
/** 是否为可编辑字段 */
|
|
123
|
+
isField: boolean
|
|
124
|
+
/** 占位符文本 (当字段为空时显示) */
|
|
125
|
+
placeholder?: string
|
|
126
|
+
/** 字段索引 (用于标识可编辑字段) */
|
|
127
|
+
fieldIndex?: number
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 模板编辑器属性
|
|
132
|
+
*/
|
|
133
|
+
export interface TemplateEditorProps {
|
|
134
|
+
/** 模板字符串,格式为普通文本与 [占位符] 的组合 */
|
|
135
|
+
template: string
|
|
136
|
+
/** 当前值 */
|
|
137
|
+
value?: string
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 模板编辑器事件
|
|
142
|
+
*/
|
|
143
|
+
export interface TemplateEditorEmits {
|
|
144
|
+
/** 更新值 */
|
|
145
|
+
(e: 'update:value', value: string): void
|
|
146
|
+
/** 输入事件 */
|
|
147
|
+
(e: 'input', value: string): void
|
|
148
|
+
/** 内容变更状态 - 通知父组件是否有内容 */
|
|
149
|
+
(e: 'content-status', hasContent: boolean): void
|
|
150
|
+
/** 字段激活状态变更 */
|
|
151
|
+
(e: 'field-active', isActive: boolean, index: number): void
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 模板编辑器暴露的方法
|
|
156
|
+
*/
|
|
157
|
+
export interface TemplateEditorExpose {
|
|
158
|
+
/** 激活第一个可编辑字段 */
|
|
159
|
+
activateFirstField: () => void
|
|
160
|
+
/** 重置所有字段 */
|
|
161
|
+
resetFields: () => void
|
|
162
|
+
}
|
package/src/sender/index.vue
CHANGED
|
@@ -6,6 +6,7 @@ import { useInputHandler } from './composables/useInputHandler'
|
|
|
6
6
|
import { useKeyboardHandler } from './composables/useKeyboardHandler'
|
|
7
7
|
import { useSpeechHandler } from './composables/useSpeechHandler'
|
|
8
8
|
import ActionButtons from './components/ActionButtons.vue'
|
|
9
|
+
import TemplateEditor from './components/TemplateEditor.vue'
|
|
9
10
|
import './index.less'
|
|
10
11
|
|
|
11
12
|
const props = withDefaults(defineProps<SenderProps>(), {
|
|
@@ -23,15 +24,33 @@ const props = withDefaults(defineProps<SenderProps>(), {
|
|
|
23
24
|
showWordLimit: false,
|
|
24
25
|
submitType: 'enter',
|
|
25
26
|
theme: 'light',
|
|
27
|
+
template: '',
|
|
28
|
+
hasContent: undefined,
|
|
26
29
|
})
|
|
27
30
|
|
|
28
31
|
const emit = defineEmits<SenderEmits>()
|
|
29
32
|
|
|
30
33
|
// 输入引用
|
|
31
34
|
const inputRef = ref<HTMLElement | null>(null)
|
|
35
|
+
const templateEditorRef = ref<InstanceType<typeof TemplateEditor> | null>(null)
|
|
36
|
+
|
|
37
|
+
// 是否显示模板编辑器
|
|
38
|
+
const showTemplateEditor = computed(() => !!props.template)
|
|
32
39
|
|
|
33
40
|
// 输入控制
|
|
34
|
-
const { inputValue, isComposing, clearInput }: InputHandler = useInputHandler(props, emit)
|
|
41
|
+
const { inputValue, isComposing, clearInput: originalClearInput }: InputHandler = useInputHandler(props, emit)
|
|
42
|
+
|
|
43
|
+
// 清空功能增强:同时处理模板和普通输入,并退出模板编辑模式
|
|
44
|
+
const clearInput = () => {
|
|
45
|
+
// 调用原始清空方法
|
|
46
|
+
originalClearInput()
|
|
47
|
+
|
|
48
|
+
// 如果当前是模板编辑模式,需要退出模板编辑模式
|
|
49
|
+
if (props.template) {
|
|
50
|
+
// 发出一个模板重置事件,通知父组件清除模板
|
|
51
|
+
emit('reset-template')
|
|
52
|
+
}
|
|
53
|
+
}
|
|
35
54
|
|
|
36
55
|
// 输入建议
|
|
37
56
|
const showSuggestions = ref(false)
|
|
@@ -46,6 +65,19 @@ const selectSuggestion = (value: string) => {
|
|
|
46
65
|
emit('suggestion-select', value)
|
|
47
66
|
}
|
|
48
67
|
|
|
68
|
+
// 模板相关处理
|
|
69
|
+
const handleTemplateInput = (value: string) => {
|
|
70
|
+
inputValue.value = value
|
|
71
|
+
emit('update:modelValue', value)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 激活第一个模板字段
|
|
75
|
+
const activateTemplateFirstField = () => {
|
|
76
|
+
if (templateEditorRef.value) {
|
|
77
|
+
templateEditorRef.value.activateFirstField()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
49
81
|
// 语音识别
|
|
50
82
|
const speechOptions = computed(() => ({
|
|
51
83
|
...(typeof props.speech === 'object' ? props.speech : {}),
|
|
@@ -93,8 +125,8 @@ const handleBlur = (event: FocusEvent) => {
|
|
|
93
125
|
emit('blur', event)
|
|
94
126
|
}
|
|
95
127
|
|
|
96
|
-
//
|
|
97
|
-
const
|
|
128
|
+
// 初始化自适应对象
|
|
129
|
+
const autoSize = computed(() => (props.mode === 'multiple' ? { minRows: 2, maxRows: 5 } : { maxRows: 1 }))
|
|
98
130
|
|
|
99
131
|
const justifyContent = computed(
|
|
100
132
|
(): {
|
|
@@ -115,6 +147,7 @@ const justifyContent = computed(
|
|
|
115
147
|
// 状态计算
|
|
116
148
|
const isDisabled = computed(() => props.disabled)
|
|
117
149
|
const isLoading = computed(() => props.loading)
|
|
150
|
+
const hasContent = computed(() => (props.hasContent !== undefined ? props.hasContent : !!inputValue.value))
|
|
118
151
|
|
|
119
152
|
// 样式类
|
|
120
153
|
const senderClasses = computed(() => ({
|
|
@@ -144,7 +177,9 @@ watch(inputValue, () => {
|
|
|
144
177
|
// 暴露方法
|
|
145
178
|
defineExpose({
|
|
146
179
|
focus: () => {
|
|
147
|
-
if (
|
|
180
|
+
if (showTemplateEditor.value && templateEditorRef.value) {
|
|
181
|
+
activateTemplateFirstField()
|
|
182
|
+
} else if (inputRef.value) {
|
|
148
183
|
inputRef.value.focus()
|
|
149
184
|
} else {
|
|
150
185
|
const input = document.querySelector('.tiny-input__inner') as HTMLInputElement
|
|
@@ -163,6 +198,7 @@ defineExpose({
|
|
|
163
198
|
submit: triggerSubmit,
|
|
164
199
|
startSpeech,
|
|
165
200
|
stopSpeech,
|
|
201
|
+
activateTemplateFirstField,
|
|
166
202
|
})
|
|
167
203
|
</script>
|
|
168
204
|
|
|
@@ -187,12 +223,24 @@ defineExpose({
|
|
|
187
223
|
|
|
188
224
|
<!-- 内容区域 - 确保最小宽度,不被挤占 -->
|
|
189
225
|
<div class="tiny-sender__content-area">
|
|
226
|
+
<!-- 模板编辑器 -->
|
|
227
|
+
<template v-if="showTemplateEditor">
|
|
228
|
+
<TemplateEditor
|
|
229
|
+
ref="templateEditorRef"
|
|
230
|
+
:template="template"
|
|
231
|
+
:value="inputValue"
|
|
232
|
+
@update:value="handleTemplateInput"
|
|
233
|
+
@input="handleTemplateInput"
|
|
234
|
+
/>
|
|
235
|
+
</template>
|
|
236
|
+
<!-- 普通输入框 -->
|
|
190
237
|
<tiny-input
|
|
238
|
+
v-else
|
|
191
239
|
ref="inputRef"
|
|
192
240
|
:autosize="autoSize"
|
|
193
|
-
|
|
241
|
+
type="textarea"
|
|
194
242
|
:readonly="isLoading"
|
|
195
|
-
|
|
243
|
+
resize="none"
|
|
196
244
|
v-model="inputValue"
|
|
197
245
|
:disabled="isDisabled"
|
|
198
246
|
:placeholder="placeholder"
|
|
@@ -217,7 +265,7 @@ defineExpose({
|
|
|
217
265
|
:loading="loading"
|
|
218
266
|
:disabled="isDisabled"
|
|
219
267
|
:show-clear="clearable"
|
|
220
|
-
:has-content="
|
|
268
|
+
:has-content="hasContent"
|
|
221
269
|
:speech-status="speechState"
|
|
222
270
|
:submit-type="submitType"
|
|
223
271
|
@clear="clearInput"
|
|
@@ -253,7 +301,7 @@ defineExpose({
|
|
|
253
301
|
:loading="loading"
|
|
254
302
|
:disabled="isDisabled"
|
|
255
303
|
:show-clear="clearable"
|
|
256
|
-
:has-content="
|
|
304
|
+
:has-content="hasContent"
|
|
257
305
|
:speech-status="speechState"
|
|
258
306
|
:submit-type="submitType"
|
|
259
307
|
@clear="clearInput"
|
package/src/sender/vars.less
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
--tr-sender-padding-bottom: 10px;
|
|
18
18
|
--tr-sender-padding-left: 24px;
|
|
19
19
|
--tr-sender-gap: 8px;
|
|
20
|
-
--tr-sender-input-height:
|
|
20
|
+
--tr-sender-input-height: 26px;
|
|
21
21
|
--tr-sender-input-min-height: 60px;
|
|
22
22
|
--tr-sender-icon-size: 22px;
|
|
23
23
|
--tr-sender-icon-size-small: 18px;
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
|
|
49
49
|
// 内容区域 (Content)
|
|
50
50
|
--tr-sender-content-min-width: 180px;
|
|
51
|
-
--tr-sender-content-padding: 15px 10px
|
|
51
|
+
--tr-sender-content-padding: 15px 10px 12px 24px;
|
|
52
52
|
--tr-sender-content-padding-with-prefix: 15px 10px 10px 8px;
|
|
53
53
|
--tr-sender-content-flex-grow: 1;
|
|
54
54
|
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
|
|
69
69
|
/* 暗色主题 */
|
|
70
70
|
.theme-dark,
|
|
71
|
-
[data-theme=
|
|
71
|
+
[data-theme='dark'] {
|
|
72
72
|
--tr-sender-border-color: #4c4d4f;
|
|
73
73
|
--tr-sender-bg-color: #1c1c1c;
|
|
74
74
|
--tr-sender-text-color: #ffffff;
|
|
@@ -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>
|