@opentiny/tiny-robot 0.2.0-alpha.0 → 0.2.0-alpha.2

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 (164) 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 +17 -0
  5. package/dist/bubble/index.d.ts +2 -2
  6. package/dist/bubble/index.type.d.ts +16 -18
  7. package/dist/feedback/components/SourceList.vue.d.ts +11 -0
  8. package/dist/feedback/components/index.d.ts +1 -0
  9. package/dist/feedback/index.d.ts +7 -0
  10. package/dist/feedback/index.type.d.ts +25 -0
  11. package/dist/feedback/index.vue.d.ts +13 -0
  12. package/dist/history/components/index.d.ts +2 -0
  13. package/dist/history/components/item-tag.vue.d.ts +5 -0
  14. package/dist/history/components/search-empty.vue.d.ts +7 -0
  15. package/dist/history/composables/index.d.ts +1 -0
  16. package/dist/history/composables/useEditItemTitle.d.ts +12 -0
  17. package/dist/history/index.d.ts +6 -0
  18. package/dist/history/index.type.d.ts +43 -0
  19. package/dist/history/index.vue.d.ts +2 -0
  20. package/dist/icon-button/index.d.ts +7 -0
  21. package/dist/icon-button/index.type.d.ts +6 -0
  22. package/dist/icon-button/index.vue.d.ts +6 -0
  23. package/dist/index.d.ts +10 -1
  24. package/dist/index.js +57 -27
  25. package/dist/node_modules/.pnpm/@opentiny_utils@3.22.0/node_modules/@opentiny/utils/dist/index.es.js +1335 -884
  26. package/dist/node_modules/.pnpm/@opentiny_vue-common@3.22.0/node_modules/@opentiny/vue-common/lib/index.js +660 -144
  27. package/dist/node_modules/.pnpm/@opentiny_vue-hooks@3.22.0/node_modules/@opentiny/vue-hooks/dist/src/vue-popper.js +85 -0
  28. package/dist/node_modules/.pnpm/@opentiny_vue-locale@3.22.0/node_modules/@opentiny/vue-locale/lib/index.js +1783 -0
  29. package/dist/node_modules/.pnpm/@opentiny_vue-renderless@3.22.0/node_modules/@opentiny/vue-renderless/tooltip/index.js +77 -0
  30. package/dist/node_modules/.pnpm/@opentiny_vue-renderless@3.22.0/node_modules/@opentiny/vue-renderless/tooltip/vue.js +90 -0
  31. package/dist/node_modules/.pnpm/@opentiny_vue-tooltip@3.22.0/node_modules/@opentiny/vue-tooltip/lib/index.js +176 -0
  32. package/dist/node_modules/.pnpm/@opentiny_vue-tooltip@3.22.0/node_modules/@opentiny/vue-tooltip/lib/pc.js +248 -0
  33. package/dist/node_modules/.pnpm/@vueuse_core@13.1.0_vue@3.5.13/node_modules/@vueuse/core/index.js +310 -0
  34. package/dist/node_modules/.pnpm/@vueuse_shared@13.1.0_vue@3.5.13/node_modules/@vueuse/shared/index.js +110 -0
  35. package/dist/packages/components/src/action-group/ActionGroup.vue.js +7 -0
  36. package/dist/packages/components/src/action-group/ActionGroup.vue2.js +121 -0
  37. package/dist/packages/components/src/action-group/ActionGroupItem.vue.js +14 -0
  38. package/dist/packages/components/src/action-group/ActionGroupItem.vue2.js +4 -0
  39. package/dist/packages/components/src/action-group/index.js +17 -0
  40. package/dist/packages/components/src/bubble/Bubble.vue.js +7 -0
  41. package/dist/packages/components/src/bubble/Bubble.vue2.js +76 -0
  42. package/dist/packages/components/src/bubble/BubbleList.vue.js +7 -0
  43. package/dist/packages/components/src/bubble/BubbleList.vue2.js +50 -0
  44. package/dist/packages/components/src/bubble/index.js +2 -2
  45. package/dist/packages/components/src/container/index.vue.js +2 -2
  46. package/dist/packages/components/src/container/index.vue2.js +36 -36
  47. package/dist/packages/components/src/feedback/components/SourceList.vue.js +7 -0
  48. package/dist/packages/components/src/feedback/components/SourceList.vue2.js +52 -0
  49. package/dist/packages/components/src/feedback/index.js +9 -0
  50. package/dist/packages/components/src/feedback/index.vue.js +7 -0
  51. package/dist/packages/components/src/feedback/index.vue2.js +143 -0
  52. package/dist/packages/components/src/history/components/item-tag.vue.js +7 -0
  53. package/dist/packages/components/src/history/components/item-tag.vue2.js +21 -0
  54. package/dist/packages/components/src/history/components/search-empty.vue.js +7 -0
  55. package/dist/packages/components/src/history/components/search-empty.vue2.js +20 -0
  56. package/dist/packages/components/src/history/composables/useEditItemTitle.js +43 -0
  57. package/dist/packages/components/src/history/index.js +11 -0
  58. package/dist/packages/components/src/history/index.vue.js +7 -0
  59. package/dist/packages/components/src/history/index.vue2.js +130 -0
  60. package/dist/packages/components/src/icon-button/index.js +9 -0
  61. package/dist/packages/components/src/icon-button/index.vue.js +7 -0
  62. package/dist/packages/components/src/icon-button/index.vue2.js +22 -0
  63. package/dist/packages/components/src/prompts/prompt.vue.js +2 -2
  64. package/dist/packages/components/src/prompts/prompt.vue2.js +17 -15
  65. package/dist/packages/components/src/question/components/HotQuestions.vue.js +23 -23
  66. package/dist/packages/components/src/question/index.vue.js +18 -18
  67. package/dist/packages/components/src/sender/components/TemplateEditor.vue.js +7 -0
  68. package/dist/packages/components/src/sender/components/TemplateEditor.vue2.js +175 -0
  69. package/dist/packages/components/src/sender/index.vue.js +149 -128
  70. package/dist/packages/components/src/suggestion/components/CategoryNav.vue.js +38 -0
  71. package/dist/packages/components/src/suggestion/components/CategoryNav.vue2.js +4 -0
  72. package/dist/packages/components/src/suggestion/components/SuggestionCapsule.vue.js +107 -0
  73. package/dist/packages/components/src/suggestion/components/SuggestionCapsule.vue2.js +4 -0
  74. package/dist/packages/components/src/suggestion/components/SuggestionPanel.vue.js +123 -0
  75. package/dist/packages/components/src/suggestion/components/SuggestionPanel.vue2.js +4 -0
  76. package/dist/packages/components/src/suggestion/composables/useKeyboardNavigation.js +45 -0
  77. package/dist/packages/components/src/suggestion/composables/useTriggerDetection.js +17 -0
  78. package/dist/packages/components/src/suggestion/index.js +9 -0
  79. package/dist/packages/components/src/suggestion/index.vue.js +179 -0
  80. package/dist/packages/components/src/suggestion/index.vue2.js +4 -0
  81. package/dist/packages/components/src/suggestion/utils/dom.js +18 -0
  82. package/dist/packages/svgs/dist/tiny-robot-svgs.js +306 -90
  83. package/dist/question/components/HotQuestions.vue.d.ts +2 -2
  84. package/dist/sender/components/TemplateEditor.vue.d.ts +18 -0
  85. package/dist/sender/index.type.d.ts +47 -0
  86. package/dist/sender/index.vue.d.ts +70 -5
  87. package/dist/style.css +1 -1
  88. package/dist/suggestion/components/CategoryNav.vue.d.ts +45 -0
  89. package/dist/suggestion/components/SuggestionCapsule.vue.d.ts +32 -0
  90. package/dist/suggestion/components/SuggestionPanel.vue.d.ts +84 -0
  91. package/dist/suggestion/composables/useKeyboardNavigation.d.ts +18 -0
  92. package/dist/suggestion/composables/useSuggestionFilter.d.ts +10 -0
  93. package/dist/suggestion/composables/useTriggerDetection.d.ts +11 -0
  94. package/dist/suggestion/index.d.ts +7 -0
  95. package/dist/suggestion/index.type.d.ts +94 -0
  96. package/dist/suggestion/index.vue.d.ts +343 -0
  97. package/dist/suggestion/utils/dom.d.ts +20 -0
  98. package/package.json +4 -3
  99. package/src/action-group/ActionGroup.vue +247 -0
  100. package/src/action-group/ActionGroupItem.vue +9 -0
  101. package/src/action-group/index.ts +25 -0
  102. package/src/action-group/index.type.ts +21 -0
  103. package/src/bubble/Bubble.vue +153 -0
  104. package/src/bubble/BubbleList.vue +55 -0
  105. package/src/bubble/index.ts +2 -2
  106. package/src/bubble/index.type.ts +7 -21
  107. package/src/container/index.vue +10 -36
  108. package/src/feedback/components/SourceList.vue +112 -0
  109. package/src/feedback/components/index.ts +1 -0
  110. package/src/feedback/index.ts +12 -0
  111. package/src/feedback/index.type.ts +27 -0
  112. package/src/feedback/index.vue +166 -0
  113. package/src/history/components/index.ts +2 -0
  114. package/src/history/components/item-tag.vue +49 -0
  115. package/src/history/components/search-empty.vue +38 -0
  116. package/src/history/composables/index.ts +1 -0
  117. package/src/history/composables/useEditItemTitle.ts +75 -0
  118. package/src/history/index.ts +12 -0
  119. package/src/history/index.type.ts +50 -0
  120. package/src/history/index.vue +292 -0
  121. package/src/icon-button/index.ts +12 -0
  122. package/src/icon-button/index.type.ts +7 -0
  123. package/src/icon-button/index.vue +58 -0
  124. package/src/index.ts +33 -1
  125. package/src/prompts/prompt.vue +7 -21
  126. package/src/question/components/HotQuestions.vue +1 -1
  127. package/src/question/index.less +9 -10
  128. package/src/sender/components/TemplateEditor.vue +465 -0
  129. package/src/sender/index.less +17 -7
  130. package/src/sender/index.type.ts +51 -0
  131. package/src/sender/index.vue +56 -8
  132. package/src/sender/vars.less +3 -3
  133. package/src/suggestion/components/CategoryNav.vue +38 -0
  134. package/src/suggestion/components/SuggestionCapsule.vue +183 -0
  135. package/src/suggestion/components/SuggestionPanel.vue +147 -0
  136. package/src/suggestion/composables/useKeyboardNavigation.ts +101 -0
  137. package/src/suggestion/composables/useSuggestionFilter.ts +34 -0
  138. package/src/suggestion/composables/useTriggerDetection.ts +46 -0
  139. package/src/suggestion/index.less +497 -0
  140. package/src/suggestion/index.ts +12 -0
  141. package/src/suggestion/index.type.ts +101 -0
  142. package/src/suggestion/index.vue +338 -0
  143. package/src/suggestion/utils/dom.ts +66 -0
  144. package/src/suggestion/vars.less +141 -0
  145. package/dist/bubble/components/actions/copy.vue.d.ts +0 -10
  146. package/dist/bubble/components/actions/index.d.ts +0 -2
  147. package/dist/bubble/components/actions/refresh.vue.d.ts +0 -2
  148. package/dist/bubble/useScroll.d.ts +0 -4
  149. package/dist/packages/components/src/bubble/bubble-list.vue.js +0 -7
  150. package/dist/packages/components/src/bubble/bubble-list.vue2.js +0 -37
  151. package/dist/packages/components/src/bubble/bubble.vue.js +0 -7
  152. package/dist/packages/components/src/bubble/bubble.vue2.js +0 -118
  153. package/dist/packages/components/src/bubble/components/actions/copy.vue.js +0 -7
  154. package/dist/packages/components/src/bubble/components/actions/copy.vue2.js +0 -35
  155. package/dist/packages/components/src/bubble/components/actions/refresh.vue.js +0 -7
  156. package/dist/packages/components/src/bubble/components/actions/refresh.vue2.js +0 -16
  157. package/dist/packages/components/src/bubble/useScroll.js +0 -13
  158. package/src/bubble/bubble-list.vue +0 -42
  159. package/src/bubble/bubble.vue +0 -247
  160. package/src/bubble/components/actions/copy.vue +0 -54
  161. package/src/bubble/components/actions/index.ts +0 -2
  162. package/src/bubble/components/actions/refresh.vue +0 -31
  163. package/src/bubble/useScroll.ts +0 -14
  164. /package/dist/bubble/{bubble-list.vue.d.ts → BubbleList.vue.d.ts} +0 -0
@@ -0,0 +1,465 @@
1
+ <script setup lang="ts">
2
+ import { ref, nextTick, watch, onMounted } 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
+ const editableRefs = ref<Record<number, HTMLElement>>({})
16
+
17
+ // 当前激活的输入块索引
18
+ const activeFieldIndex = ref<number>(-1)
19
+
20
+ // 是否有内容标志
21
+ const hasContent = ref(false)
22
+
23
+ // 标记是否是用户手动编辑
24
+ const isUserEditing = ref(false)
25
+
26
+ // 标记是否是内部更新,避免循环更新
27
+ const isInternalUpdate = ref(false)
28
+
29
+ // 使用ref代替computed属性,作为组件的主要状态来源
30
+ const editableParts = ref<TemplatePart[]>([])
31
+
32
+ // 解析模板,将其分解为普通文本和可编辑字段
33
+ const parseTemplate = (template: string): TemplatePart[] => {
34
+ const parts: TemplatePart[] = []
35
+ let currentIndex = 0
36
+ let fieldIndex = 0
37
+
38
+ // 正则表达式匹配 [xxx] 格式
39
+ const regex = /\[(.*?)\]/g
40
+ let match
41
+
42
+ while ((match = regex.exec(template)) !== null) {
43
+ // 添加匹配前的普通文本
44
+ if (match.index > currentIndex) {
45
+ parts.push({
46
+ content: template.substring(currentIndex, match.index),
47
+ isField: false,
48
+ })
49
+ }
50
+
51
+ // 添加匹配的字段,将placeholder作为默认内容
52
+ parts.push({
53
+ content: match[1], // 使用占位符作为默认内容
54
+ placeholder: match[1],
55
+ isField: true,
56
+ fieldIndex: fieldIndex++,
57
+ })
58
+
59
+ currentIndex = match.index + match[0].length
60
+ }
61
+
62
+ // 添加剩余的普通文本
63
+ if (currentIndex < template.length) {
64
+ parts.push({
65
+ content: template.substring(currentIndex),
66
+ isField: false,
67
+ })
68
+ }
69
+
70
+ return parts
71
+ }
72
+
73
+ // 初始化editableParts
74
+ const initializeEditableParts = (): void => {
75
+ editableParts.value = parseTemplate(props.template)
76
+ if (props.value) {
77
+ updatePartsFromValue(props.value)
78
+ }
79
+ }
80
+
81
+ // 从value更新各个部分的内容
82
+ const updatePartsFromValue = (value: string): void => {
83
+ if (!value || isInternalUpdate.value) return
84
+
85
+ // 仅在值确实不同时才更新,避免不必要的处理
86
+ const currentValue = generateValue()
87
+ if (currentValue === value) return
88
+
89
+ // 创建一个临时的解析结果用于比较
90
+ const templateStructure = parseTemplate(props.template)
91
+
92
+ // 计算模板的静态部分(非字段部分)
93
+ const staticParts: string[] = []
94
+
95
+ templateStructure.forEach((part) => {
96
+ if (!part.isField) {
97
+ staticParts.push(part.content)
98
+ } else {
99
+ // 用一个占位符标记字段位置
100
+ staticParts.push(`__FIELD_${part.fieldIndex}__`)
101
+ }
102
+ })
103
+
104
+ // 使用静态部分作为分隔符,尝试提取字段值
105
+ let remainingValue = value
106
+ let currentFieldIndex = 0
107
+
108
+ // 遍历editableParts,找到字段并更新
109
+ for (let i = 0; i < editableParts.value.length; i++) {
110
+ const part = editableParts.value[i]
111
+
112
+ if (!part.isField) continue
113
+
114
+ // 尝试找到当前字段前的静态部分
115
+ const prevStaticPart = staticParts[currentFieldIndex]
116
+ currentFieldIndex++
117
+
118
+ // 下一个静态部分(如果有)
119
+ const nextStaticPart = staticParts[currentFieldIndex] || ''
120
+
121
+ if (prevStaticPart && remainingValue.startsWith(prevStaticPart)) {
122
+ // 移除前导静态部分
123
+ remainingValue = remainingValue.substring(prevStaticPart.length)
124
+
125
+ // 如果有下一个静态部分,提取中间的字段值
126
+ if (nextStaticPart && remainingValue.includes(nextStaticPart)) {
127
+ const fieldEndIndex = remainingValue.indexOf(nextStaticPart)
128
+ const fieldValue = remainingValue.substring(0, fieldEndIndex)
129
+
130
+ // 更新字段内容
131
+ if (fieldValue) {
132
+ editableParts.value[i].content = fieldValue
133
+
134
+ // 如果当前字段是激活状态,更新DOM内容,但不改变光标位置
135
+ if (activeFieldIndex.value === i && editableRefs.value[i]) {
136
+ // 使用MutationObserver来禁用临时
137
+ isUserEditing.value = true
138
+ // 不要直接设置textContent,避免干扰用户输入
139
+ }
140
+ }
141
+
142
+ // 移除已处理的部分
143
+ remainingValue = remainingValue.substring(fieldEndIndex)
144
+ } else if (!nextStaticPart && remainingValue) {
145
+ // 如果是最后一个字段,则剩余的全部是它的值
146
+ editableParts.value[i].content = remainingValue
147
+ remainingValue = ''
148
+ }
149
+ }
150
+ }
151
+
152
+ // 更新内容状态
153
+ checkHasContent(value)
154
+ }
155
+
156
+ // 生成完整的文本值
157
+ const generateValue = (): string => {
158
+ return editableParts.value.map((part) => part.content).join('')
159
+ }
160
+
161
+ // 更新值并触发事件
162
+ const updateValue = (): void => {
163
+ isInternalUpdate.value = true
164
+ const newValue = generateValue()
165
+ emit('update:value', newValue)
166
+ emit('input', newValue)
167
+ checkHasContent(newValue)
168
+
169
+ // 延迟重置标志,防止循环更新
170
+ setTimeout(() => {
171
+ isInternalUpdate.value = false
172
+ }, 0)
173
+ }
174
+
175
+ // 检查是否有内容
176
+ const checkHasContent = (value?: string): void => {
177
+ const contentValue = value || generateValue()
178
+ const newHasContent = contentValue.trim().length > 0
179
+
180
+ if (hasContent.value !== newHasContent) {
181
+ hasContent.value = newHasContent
182
+ emit('content-status', newHasContent)
183
+ }
184
+ }
185
+
186
+ // 注册可编辑元素引用
187
+ const registerEditableRef = (el: HTMLSpanElement | null, index: number): void => {
188
+ if (el) {
189
+ // 保存引用
190
+ editableRefs.value[index] = el
191
+
192
+ // 设置初始内容 (只在初始时设置,后续由用户输入控制)
193
+ if (!el.textContent) {
194
+ el.textContent = editableParts.value[index].content
195
+ }
196
+
197
+ // 使用MutationObserver监听内容变化,但避免循环更新
198
+ const observer = new MutationObserver(() => {
199
+ if (activeFieldIndex.value === index && !isUserEditing.value) {
200
+ isUserEditing.value = true
201
+
202
+ // 延迟更新模型,避免与Vue渲染循环冲突
203
+ setTimeout(() => {
204
+ editableParts.value[index].content = el.textContent !== null ? el.textContent : ''
205
+ isUserEditing.value = false
206
+ }, 0)
207
+ }
208
+ })
209
+
210
+ observer.observe(el, {
211
+ characterData: true,
212
+ childList: true,
213
+ subtree: true,
214
+ })
215
+ }
216
+ }
217
+
218
+ // 处理内容输入事件
219
+ const handleContentInput = (event: Event, index: number): void => {
220
+ isUserEditing.value = true
221
+
222
+ // 获取当前输入内容
223
+ const target = event.target as HTMLElement
224
+ const content = target.textContent !== null ? target.textContent : ''
225
+
226
+ // 更新模型,但不触发DOM更新
227
+ if (editableParts.value[index].content !== content) {
228
+ editableParts.value[index].content = content
229
+ updateValue()
230
+ }
231
+
232
+ // 通过延时重置标志,防止与MutationObserver冲突
233
+ setTimeout(() => {
234
+ isUserEditing.value = false
235
+ }, 10)
236
+ }
237
+
238
+ // 将光标放到元素内容末尾
239
+ const placeCaretAtEnd = (el: HTMLElement): void => {
240
+ if (document.createRange) {
241
+ const range = document.createRange()
242
+ range.selectNodeContents(el)
243
+ // false表示光标放到末尾
244
+ range.collapse(false)
245
+
246
+ const selection = window.getSelection()
247
+ if (selection) {
248
+ selection.removeAllRanges()
249
+ selection.addRange(range)
250
+ }
251
+ }
252
+ }
253
+
254
+ // 激活输入块
255
+ const activateField = (index: number): void => {
256
+ // 如果点击的就是当前激活的字段,不做任何处理
257
+ if (activeFieldIndex.value === index) {
258
+ return
259
+ }
260
+
261
+ // 如果当前已有激活的字段,先保存它的值
262
+ if (activeFieldIndex.value !== -1) {
263
+ const currentIndex = activeFieldIndex.value
264
+ const el = editableRefs.value[currentIndex]
265
+ if (el) {
266
+ editableParts.value[currentIndex].content = el.textContent !== null ? el.textContent : ''
267
+ updateValue()
268
+ }
269
+ }
270
+
271
+ // 设置激活状态
272
+ activeFieldIndex.value = index
273
+ emit('field-active', true, index)
274
+
275
+ // 确保DOM更新后再聚焦
276
+ nextTick(() => {
277
+ const el = editableRefs.value[index]
278
+ if (el) {
279
+ // 先获取焦点
280
+ el.focus()
281
+
282
+ // 确保元素有内容以便放置光标
283
+ if (!el.textContent && editableParts.value[index].content) {
284
+ el.textContent = editableParts.value[index].content
285
+ }
286
+
287
+ // 将光标放到末尾
288
+ placeCaretAtEnd(el)
289
+ }
290
+ })
291
+ }
292
+
293
+ // 取消激活输入块
294
+ const deactivateField = (): void => {
295
+ const currentIndex = activeFieldIndex.value
296
+ if (currentIndex >= 0) {
297
+ // 更新当前编辑的内容
298
+ const el = editableRefs.value[currentIndex]
299
+ if (el) {
300
+ // 使用实际内容,不回退到占位符
301
+ const content = el.textContent !== null ? el.textContent : ''
302
+ editableParts.value[currentIndex].content = content
303
+ }
304
+
305
+ emit('field-active', false, currentIndex)
306
+ }
307
+
308
+ updateValue()
309
+ activeFieldIndex.value = -1
310
+ }
311
+
312
+ // 处理输入框键盘事件
313
+ const handleInputKeyDown = (event: KeyboardEvent): void => {
314
+ // 只处理Enter键
315
+ if (event.key === 'Enter') {
316
+ event.preventDefault()
317
+ deactivateField()
318
+ }
319
+ }
320
+
321
+ // 监听值变化
322
+ watch(
323
+ () => props.value,
324
+ (newValue) => {
325
+ // 如果外部值为空字符串,重置为默认placeholder值
326
+ if (newValue === '') {
327
+ resetToDefaultValues()
328
+ } else if (newValue && !isInternalUpdate.value) {
329
+ // 如果新值不为空,并且不是内部更新触发的,更新各部分内容
330
+ updatePartsFromValue(newValue)
331
+ }
332
+ },
333
+ )
334
+
335
+ // 监听模板变化
336
+ watch(
337
+ () => props.template,
338
+ () => {
339
+ // 当模板变化时,重新初始化
340
+ initializeEditableParts()
341
+ // 检查内容状态
342
+ nextTick(() => {
343
+ checkHasContent()
344
+ })
345
+ },
346
+ { immediate: true },
347
+ )
348
+
349
+ // 重置所有字段内容为默认值
350
+ const resetToDefaultValues = (): void => {
351
+ editableParts.value.forEach((part) => {
352
+ if (part.isField) {
353
+ part.content = part.placeholder || ''
354
+ }
355
+ })
356
+ // 更新值
357
+ updateValue()
358
+ }
359
+
360
+ // 重置所有字段内容
361
+ const resetFields = (): void => {
362
+ resetToDefaultValues()
363
+ }
364
+
365
+ // 组件挂载时
366
+ onMounted(() => {
367
+ // 初始化内容状态
368
+ checkHasContent()
369
+ })
370
+
371
+ // 导出方法供父组件调用
372
+ defineExpose<TemplateEditorExpose>({
373
+ activateFirstField: () => {
374
+ for (let i = 0; i < editableParts.value.length; i++) {
375
+ if (editableParts.value[i].isField) {
376
+ activateField(i)
377
+ break
378
+ }
379
+ }
380
+ },
381
+ resetFields,
382
+ })
383
+ </script>
384
+
385
+ <template>
386
+ <div class="template-editor">
387
+ <div class="template-content" ref="editorRef">
388
+ <template v-for="(part, index) in editableParts" :key="index">
389
+ <!-- 普通文本部分 -->
390
+ <span v-if="!part.isField">{{ part.content }}</span>
391
+
392
+ <!-- 可编辑的输入块部分 -->
393
+ <span
394
+ v-else
395
+ class="template-field"
396
+ :class="{ 'template-field-active': activeFieldIndex === index }"
397
+ @click="activateField(index)"
398
+ >
399
+ <!-- 编辑状态显示 -->
400
+ <span
401
+ v-if="activeFieldIndex === index"
402
+ :ref="(el) => registerEditableRef(el as HTMLSpanElement, index)"
403
+ class="template-editable"
404
+ contenteditable="true"
405
+ @input="handleContentInput($event, index)"
406
+ @blur="deactivateField()"
407
+ @keydown="handleInputKeyDown($event)"
408
+ @click.stop
409
+ ></span>
410
+ <!-- 非编辑状态显示 -->
411
+ <span v-else class="template-placeholder">{{ part.content }}</span>
412
+ </span>
413
+ </template>
414
+ </div>
415
+ </div>
416
+ </template>
417
+
418
+ <style lang="less" scoped>
419
+ .template-editor {
420
+ width: 100%;
421
+ min-height: 26px;
422
+ line-height: 26px;
423
+ }
424
+
425
+ .template-content {
426
+ display: inline;
427
+ word-break: break-word;
428
+ }
429
+
430
+ .template-field {
431
+ display: inline-block;
432
+ height: 26px;
433
+ min-width: fit-content;
434
+ border-radius: 4px;
435
+ background: rgba(0, 0, 0, 0.05);
436
+ padding: 0 10px;
437
+ margin: 0 2px;
438
+ vertical-align: middle;
439
+ cursor: text;
440
+ transition: all 0.2s ease;
441
+
442
+ &-active {
443
+ background-color: #ffffff;
444
+ border: 1px solid rgba(194, 194, 194);
445
+ border-radius: 4px;
446
+ }
447
+
448
+ .template-placeholder {
449
+ color: #333;
450
+ user-select: none;
451
+ }
452
+ }
453
+
454
+ .template-editable {
455
+ display: inline-block;
456
+ min-width: 1px; // 防止编辑框过窄
457
+ outline: none;
458
+ white-space: nowrap;
459
+ color: #333;
460
+
461
+ &:focus {
462
+ outline: none;
463
+ }
464
+ }
465
+ </style>
@@ -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: var(--tr-sender-input-height);
43
- padding-left: 0;
44
- padding-right: 0;
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-top) var(--tr-sender-padding-right) var(--tr-sender-padding-bottom)
287
- var(--tr-sender-padding-left);
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;
@@ -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
+ }