@houaoran/designer 1.0.0

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 (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/dist/components.es.js +11424 -0
  4. package/dist/components.umd.js +878 -0
  5. package/dist/index.es.js +39113 -0
  6. package/dist/index.umd.js +1187 -0
  7. package/package.json +96 -0
  8. package/src/components/DragBox.vue +49 -0
  9. package/src/components/DragTool.vue +235 -0
  10. package/src/components/EventConfig.vue +557 -0
  11. package/src/components/FcDesigner.vue +2569 -0
  12. package/src/components/FcTitle.vue +69 -0
  13. package/src/components/FetchConfig.vue +415 -0
  14. package/src/components/FieldInput.vue +371 -0
  15. package/src/components/FnConfig.vue +315 -0
  16. package/src/components/FnEditor.vue +327 -0
  17. package/src/components/FnInput.vue +103 -0
  18. package/src/components/FormLabel.vue +47 -0
  19. package/src/components/HtmlEditor.vue +125 -0
  20. package/src/components/JsonPreview.vue +146 -0
  21. package/src/components/OptionsTextInput.vue +151 -0
  22. package/src/components/PropsInput.vue +72 -0
  23. package/src/components/Required.vue +75 -0
  24. package/src/components/Row.vue +26 -0
  25. package/src/components/SignaturePad.vue +176 -0
  26. package/src/components/Struct.vue +153 -0
  27. package/src/components/StructEditor.vue +121 -0
  28. package/src/components/StructTree.vue +209 -0
  29. package/src/components/TableOptions.vue +164 -0
  30. package/src/components/TreeOptions.vue +167 -0
  31. package/src/components/TypeSelect.vue +144 -0
  32. package/src/components/Validate.vue +302 -0
  33. package/src/components/ValueInput.vue +89 -0
  34. package/src/components/Warning.vue +46 -0
  35. package/src/components/ai/AiPanel.vue +1122 -0
  36. package/src/components/ai/MarkdownRenderer.vue +548 -0
  37. package/src/components/language/LanguageConfig.vue +174 -0
  38. package/src/components/language/LanguageInput.vue +191 -0
  39. package/src/components/style/BackgroundInput.vue +315 -0
  40. package/src/components/style/BorderInput.vue +242 -0
  41. package/src/components/style/BoxSizeInput.vue +166 -0
  42. package/src/components/style/BoxSpaceInput.vue +269 -0
  43. package/src/components/style/ColorInput.vue +90 -0
  44. package/src/components/style/ConfigItem.vue +118 -0
  45. package/src/components/style/FontInput.vue +197 -0
  46. package/src/components/style/PositionInput.vue +146 -0
  47. package/src/components/style/RadiusInput.vue +164 -0
  48. package/src/components/style/ShadowContent.vue +335 -0
  49. package/src/components/style/ShadowInput.vue +91 -0
  50. package/src/components/style/SizeInput.vue +118 -0
  51. package/src/components/style/StyleConfig.vue +307 -0
  52. package/src/components/table/Table.vue +252 -0
  53. package/src/components/table/TableView.vue +1058 -0
  54. package/src/components/tableForm/TableForm.vue +471 -0
  55. package/src/components/tableForm/TableFormColumnView.vue +103 -0
  56. package/src/components/tableForm/TableFormView.vue +46 -0
  57. package/src/components/tree/FcTree.vue +713 -0
  58. package/src/components/tree/FcTreeNode.vue +216 -0
  59. package/src/config/base/field.js +43 -0
  60. package/src/config/base/form.js +132 -0
  61. package/src/config/base/style.js +26 -0
  62. package/src/config/base/validate.js +15 -0
  63. package/src/config/index.js +70 -0
  64. package/src/config/menu.js +24 -0
  65. package/src/config/rule/alert.js +45 -0
  66. package/src/config/rule/button.js +49 -0
  67. package/src/config/rule/card.js +40 -0
  68. package/src/config/rule/cascader.js +121 -0
  69. package/src/config/rule/checkbox.js +68 -0
  70. package/src/config/rule/col.js +86 -0
  71. package/src/config/rule/collapse.js +30 -0
  72. package/src/config/rule/collapseItem.js +36 -0
  73. package/src/config/rule/color.js +53 -0
  74. package/src/config/rule/date.js +66 -0
  75. package/src/config/rule/dateRange.js +60 -0
  76. package/src/config/rule/divider.js +31 -0
  77. package/src/config/rule/editor.js +31 -0
  78. package/src/config/rule/group.js +86 -0
  79. package/src/config/rule/html.js +43 -0
  80. package/src/config/rule/image.js +32 -0
  81. package/src/config/rule/input.js +62 -0
  82. package/src/config/rule/number.js +49 -0
  83. package/src/config/rule/password.js +52 -0
  84. package/src/config/rule/radio.js +43 -0
  85. package/src/config/rule/rate.js +44 -0
  86. package/src/config/rule/row.js +46 -0
  87. package/src/config/rule/select.js +70 -0
  88. package/src/config/rule/signaturePad.js +59 -0
  89. package/src/config/rule/slider.js +53 -0
  90. package/src/config/rule/space.js +44 -0
  91. package/src/config/rule/subForm.js +47 -0
  92. package/src/config/rule/switch.js +46 -0
  93. package/src/config/rule/tabPane.js +29 -0
  94. package/src/config/rule/table.js +37 -0
  95. package/src/config/rule/tableForm.js +115 -0
  96. package/src/config/rule/tableFormColumn.js +55 -0
  97. package/src/config/rule/tabs.js +38 -0
  98. package/src/config/rule/tag.js +69 -0
  99. package/src/config/rule/text.js +41 -0
  100. package/src/config/rule/textarea.js +63 -0
  101. package/src/config/rule/time.js +58 -0
  102. package/src/config/rule/timeRange.js +49 -0
  103. package/src/config/rule/title.js +37 -0
  104. package/src/config/rule/transfer.js +59 -0
  105. package/src/config/rule/tree.js +70 -0
  106. package/src/config/rule/treeSelect.js +77 -0
  107. package/src/config/rule/upload.js +107 -0
  108. package/src/form/index.js +19 -0
  109. package/src/index.js +173 -0
  110. package/src/locale/en.js +981 -0
  111. package/src/locale/zh-cn.js +983 -0
  112. package/src/style/fonts/fc-icons.woff +0 -0
  113. package/src/style/icon.css +1052 -0
  114. package/src/style/index.css +836 -0
  115. package/src/utils/form.js +9 -0
  116. package/src/utils/highlight/highlight.min.js +307 -0
  117. package/src/utils/highlight/javascript.min.js +80 -0
  118. package/src/utils/highlight/style.css +1 -0
  119. package/src/utils/highlight/xml.min.js +29 -0
  120. package/src/utils/hintStubs.js +120 -0
  121. package/src/utils/index.js +544 -0
  122. package/src/utils/jsonDiff.js +173 -0
  123. package/src/utils/locale.js +23 -0
  124. package/src/utils/message.js +19 -0
  125. package/src/utils/template.js +105 -0
  126. package/types/index.d.ts +575 -0
@@ -0,0 +1,1122 @@
1
+ <template>
2
+ <div class="_fd-ai-panel">
3
+ <!-- 对话头部 -->
4
+ <div class="_fd-ai-header">
5
+ <div class="_fd-ai-title">
6
+ <i class="fc-icon icon-ai bright"></i>
7
+ <span class="_fd-ai-name">{{ t('ai.name') }}</span>
8
+ </div>
9
+ <div class="_fd-ai-actions" v-if="messages.length">
10
+ <div class="_fd-ai-action-btn" @click="clearChat">
11
+ <i class="fc-icon icon-delete2"></i>
12
+ </div>
13
+ </div>
14
+ </div>
15
+
16
+ <!-- 对话内容区域 -->
17
+ <div class="_fd-ai-content" :class="{ '_fd-ai-content--centered': messages.length === 0 }" ref="chatContent">
18
+ <!-- 欢迎消息 -->
19
+ <div class="_fd-ai-welcome" v-if="messages.length === 0">
20
+ <div class="_fd-ai-welcome-icon"></div>
21
+ <h3>{{ t('ai.welcome') }}</h3>
22
+ <p>{{ t('ai.info') }}</p>
23
+
24
+ <!-- 示例问题 -->
25
+ <div class="_fd-ai-welcome-suggestions">
26
+ <div class="_fd-ai-suggestions-header">
27
+ <span>{{ t('ai.try') }}</span>
28
+ <div class="_fd-ai-refresh-btn" @click="refreshSuggestions">
29
+ <i class="fc-icon icon-refresh2"></i>
30
+ {{ t('ai.change') }}
31
+ </div>
32
+ </div>
33
+ <div class="_fd-ai-suggestions-list">
34
+ <div
35
+ v-for="(suggestion, index) in suggestions"
36
+ :key="index"
37
+ class="_fd-ai-suggestion-item"
38
+ @click="selectSuggestion(suggestion)"
39
+ >
40
+ {{ suggestion }}
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <!-- 对话消息列表 -->
47
+ <div class="_fd-ai-messages">
48
+ <div v-for="(message, index) in messages" :key="index"
49
+ :class="['_fd-ai-message', `_fd-ai-message--${message.type}`]">
50
+ <!-- 用户消息 -->
51
+ <div v-if="message.type === 'user'" class="_fd-ai-message-content">
52
+ <div class="_fd-ai-message-bubble _fd-ai-message-bubble--user">
53
+ <div class="_fd-ai-message-text">{{ message.content }}</div>
54
+ </div>
55
+
56
+ <!-- 底部悬浮区域 -->
57
+ <div class="_fd-ai-message-footer">
58
+ <!-- 操作按钮 -->
59
+ <div class="_fd-ai-message-actions">
60
+ <div class="_fd-ai-action-btn" @click="rmMessage(index)">
61
+ <i class="fc-icon icon-delete2"></i>
62
+ </div>
63
+ <div class="_fd-ai-action-btn" @click="copyMessage(message.content)">
64
+ <i class="fc-icon icon-group"></i>
65
+ </div>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ <!-- AI 复合消息(包含文字、思考链、错误状态) -->
70
+ <div v-else-if="message.type === 'ai'" class="_fd-ai-message-content">
71
+ <div class="_fd-ai-message-avatar"
72
+ :class="{ '_fd-ai-message-avatar--error': message.isError }"></div>
73
+ <div
74
+ class="_fd-ai-message-bubble"
75
+ :class="{
76
+ '_fd-ai-message-bubble--ai': !message.isError,
77
+ '_fd-ai-message-bubble--error': message.isError,
78
+ '_fd-ai-message-bubble--thinking': message.type === 'thinking',
79
+ }"
80
+ >
81
+ <template v-for="content in message.content">
82
+ <template v-if="content.type === 'tool'">
83
+ <div v-if="content.steps && content.steps.length > 0"
84
+ class="_fd-ai-thinking-section">
85
+ <div class="_fd-ai-thinking-steps">
86
+ <div
87
+ v-for="(step, stepIndex) in content.steps"
88
+ :key="stepIndex"
89
+ :class="[
90
+ '_fd-ai-thinking-step',
91
+ { '_fd-ai-thinking-step--active': isThinking && index === messages.length - 1 },
92
+ ]"
93
+ >
94
+ <div
95
+ class="_fd-ai-thinking-step-icon"
96
+ :class="{
97
+ '_fd-ai-thinking-step-icon--executing':
98
+ step.status !== 'end' && isThinking && index === messages.length - 1,
99
+ '_fd-ai-thinking-step-icon--completed':
100
+ step.status === 'end' && isThinking && index === messages.length - 1,
101
+ }"
102
+ >
103
+ <template v-if="messages.length - 1 === index && isThinking">
104
+ <svg v-if="step.status === 'end'" height="12"
105
+ viewBox="0 0 12 12" fill="none">
106
+ <path
107
+ d="M10 3L4.5 8.5L2 6"
108
+ stroke="currentColor"
109
+ stroke-width="1.5"
110
+ stroke-linecap="round"
111
+ stroke-linejoin="round"
112
+ />
113
+ </svg>
114
+ <div class="_fd-ai-thinking-step-loading" v-else></div>
115
+ </template>
116
+ <div class="_fd-ai-thinking-step-pending" v-else></div>
117
+ </div>
118
+ <span class="_fd-ai-thinking-step-text">{{ step.title }}</span>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </template>
123
+ <template v-if="content.type === 'text'">
124
+ <div v-if="content.text" class="_fd-ai-message-text">
125
+ <MarkdownRenderer :content="content.text"/>
126
+ </div>
127
+ </template>
128
+ </template>
129
+ <div v-if="message.isError" class="_fd-ai-message-text _fd-ai-message-text--error">
130
+ {{ t('ai.error') }}
131
+ </div>
132
+ <!-- 思考中状态 -->
133
+ <div class="_fd-ai-thinking-status" v-if="message.status === 'thinking'">
134
+ <div class="_fd-ai-thinking-indicator">
135
+ <div class="_fd-ai-thinking-dot"></div>
136
+ <div class="_fd-ai-thinking-dot"></div>
137
+ <div class="_fd-ai-thinking-dot"></div>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ <!-- 底部悬浮区域 -->
142
+ <div class="_fd-ai-message-footer" v-if="!isThinking || index !== messages.length - 1">
143
+ <!-- 操作按钮 -->
144
+ <div class="_fd-ai-message-actions">
145
+ <div class="_fd-ai-action-btn" @click="rmMessage(index)">
146
+ <i class="fc-icon icon-delete2"></i>
147
+ </div>
148
+ <div class="_fd-ai-action-btn" @click="copyMessage(message.content)">
149
+ <i class="fc-icon icon-group"></i>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ </div>
157
+
158
+ <!-- 输入区域 -->
159
+ <div class="_fd-ai-input">
160
+ <!-- 用户输入框 -->
161
+ <div class="_fd-ai-input-container">
162
+ <textarea
163
+ v-model="inputText"
164
+ @keydown="handleKeydown"
165
+ @input="handleInput"
166
+ :placeholder="t('ai.placeholder')"
167
+ class="_fd-ai-input-field"
168
+ :disabled="isThinking"
169
+ ></textarea>
170
+ <div
171
+ class="_fd-ai-send-btn"
172
+ :class="{ '_fd-ai-send-btn--disabled': !inputText.trim() && !isThinking }"
173
+ @click="sendMessage"
174
+ >
175
+ <div class="fc-icon icon-suspend" v-if="isThinking"></div>
176
+ <div class="fc-icon icon-send" v-else></div>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </template>
182
+
183
+ <script>
184
+ import MarkdownRenderer from './MarkdownRenderer.vue';
185
+ import {copyTextToClipboard} from '../../utils';
186
+ import viewForm from '../../utils/form';
187
+
188
+ export default {
189
+ name: 'FcAiPanel',
190
+ inject: ['designer'],
191
+ components: {
192
+ MarkdownRenderer,
193
+ },
194
+ data() {
195
+ return {
196
+ page: 0,
197
+ inputText: '',
198
+ controller: null,
199
+ isThinking: false,
200
+ isUserAtBottom: true,
201
+ suggestions: [],
202
+ messages: [],
203
+ };
204
+ },
205
+ computed: {
206
+ t() {
207
+ return this.designer.setupState.t;
208
+ },
209
+ api() {
210
+ return this.designer.props.config?.ai?.api || 'https://api.form-create.com/ai/v2/chat/form';
211
+ },
212
+ token() {
213
+ let token = this.designer.props.config?.ai?.token || '';
214
+ if (token && token.indexOf('Bearer') === -1) {
215
+ token = `Bearer ${token}`;
216
+ }
217
+ return token;
218
+ },
219
+ },
220
+ methods: {
221
+ sendMessage() {
222
+ if (this.isThinking) {
223
+ this.isThinking = false;
224
+ this.controller && this.controller.abort();
225
+ return;
226
+ }
227
+ if (!this.inputText.trim()) return;
228
+
229
+ const userMessage = {
230
+ type: 'user',
231
+ content: this.inputText.trim(),
232
+ timestamp: new Date(),
233
+ };
234
+
235
+ this.messages.push(userMessage);
236
+ this.inputText = '';
237
+
238
+ // 发送消息后强制滚动到底部
239
+ this.isUserAtBottom = true;
240
+ this.$nextTick(() => {
241
+ this.scrollToBottom();
242
+ });
243
+
244
+ this.simulateThinking();
245
+ },
246
+
247
+ async simulateThinking() {
248
+ this.isThinking = true;
249
+
250
+ const aiMessage = {
251
+ type: 'ai',
252
+ content: [],
253
+ status: 'thinking',
254
+ timestamp: new Date(),
255
+ };
256
+
257
+ this.messages.push(aiMessage);
258
+ this.saveHistory();
259
+
260
+ this.callAiApi(aiMessage);
261
+ },
262
+
263
+ async callAiApi(aiMessage) {
264
+ try {
265
+ aiMessage.status = 'thinking';
266
+
267
+ // 获取最后一条用户消息
268
+ const userMessages = this.messages.filter(msg => msg.type === 'user');
269
+ const lastUserMessage = userMessages[userMessages.length - 1];
270
+ this.controller = new AbortController();
271
+ const response = await fetch(this.api, {
272
+ method: 'POST',
273
+ headers: {
274
+ 'Content-Type': 'application/json',
275
+ Authorization: this.token,
276
+ },
277
+ body: JSON.stringify({
278
+ ui: 'element-plus',
279
+ basic: true,
280
+ form: {
281
+ rule: this.designer.setupState.getJson(),
282
+ option: this.designer.setupState.getOptionsJson(),
283
+ },
284
+ messages: [
285
+ {
286
+ role: 'user',
287
+ content: lastUserMessage.content,
288
+ },
289
+ ],
290
+ }),
291
+ signal: this.controller.signal,
292
+ });
293
+
294
+ if (!response.ok) {
295
+ throw new Error(`HTTP error! status: ${response.status}`);
296
+ }
297
+
298
+ const reader = response.body.getReader();
299
+ const decoder = new TextDecoder();
300
+ let fullText = '';
301
+ let buffer = '';
302
+
303
+ while (true) {
304
+ const {done, value} = await reader.read();
305
+ if (done) {
306
+ break;
307
+ }
308
+
309
+ const chunk = decoder.decode(value, {stream: true});
310
+
311
+ buffer += chunk;
312
+ const lines = buffer.split('\n');
313
+ buffer = lines.pop() || ''; // 保留不完整的行
314
+
315
+ for (const line of lines) {
316
+ const trimmedLine = line.trim();
317
+ if (trimmedLine.startsWith('data: ')) {
318
+ const data = trimmedLine.slice(6);
319
+
320
+ if (data === '[DONE]') {
321
+ aiMessage.status = 'completed';
322
+ this.isThinking = false;
323
+ this.$forceUpdate();
324
+ this.saveHistory();
325
+ return;
326
+ }
327
+
328
+ try {
329
+ const parsed = JSON.parse(data);
330
+
331
+ let content = parsed.choices?.[0]?.delta?.content;
332
+ if (content) {
333
+ if (content.startsWith('[FC_TOOL]')) {
334
+ const tool = JSON.parse(content.replace('[FC_TOOL]', ''));
335
+ this.updateStep(aiMessage, tool);
336
+ fullText = '';
337
+ } else {
338
+ if (content.trim().startsWith('```fcRuleDiff')) {
339
+ content = `\n\`\`\`fcRuleDiff\n${JSON.stringify({
340
+ newJson: content.trim().slice(13, -3).trim(),
341
+ oldJson: viewForm.toJson({rule: this.designer.setupState.getRule()}),
342
+ })}\n\`\`\`\n`;
343
+ }
344
+ fullText += content;
345
+ let message = aiMessage.content[aiMessage.content.length - 1];
346
+ if (!message || message.type !== 'text') {
347
+ message = {type: 'text', text: ''};
348
+ aiMessage.content.push(message);
349
+ }
350
+ message.text = fullText.trim();
351
+ }
352
+
353
+ this.$forceUpdate();
354
+ }
355
+ } catch (e) {
356
+ console.warn('解析流数据失败:', e, '数据:', data);
357
+ }
358
+ }
359
+ }
360
+ }
361
+
362
+ aiMessage.status = 'completed';
363
+ this.isThinking = false;
364
+ this.$forceUpdate();
365
+ } catch (error) {
366
+ console.error('AI API 调用失败:', error);
367
+ this.isThinking = false;
368
+ if (error.name === 'AbortError') {
369
+ aiMessage.status = 'completed';
370
+ } else {
371
+ aiMessage.status = 'error';
372
+ aiMessage.isError = true;
373
+ }
374
+ this.$forceUpdate();
375
+ }
376
+ this.saveHistory();
377
+ },
378
+ updateStep(aiMessage, tool) {
379
+ let flag = false;
380
+ if (tool.id) {
381
+ aiMessage.content.forEach(message => {
382
+ if (message.type === 'tool') {
383
+ message.steps.forEach(step => {
384
+ if (step.id === tool.id) {
385
+ step.title = tool.title;
386
+ step.status = tool.status;
387
+ flag = true;
388
+ }
389
+ });
390
+ }
391
+ });
392
+ }
393
+ if (!flag) {
394
+ let message = aiMessage.content[aiMessage.content.length - 1];
395
+ if (!message || message.type !== 'tool') {
396
+ message = {type: 'tool', steps: []};
397
+ aiMessage.content.push(message);
398
+ }
399
+ message.steps.push({
400
+ title: tool.title,
401
+ status: tool.status,
402
+ id: tool.id,
403
+ });
404
+ }
405
+ },
406
+ handleKeydown(event) {
407
+ if (event.key === 'Enter' && !event.shiftKey) {
408
+ event.preventDefault();
409
+ this.sendMessage();
410
+ }
411
+ },
412
+ handleInput() {
413
+ const textarea = event.target;
414
+ textarea.style.height = 'auto';
415
+ textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
416
+ },
417
+ copyMessage(content) {
418
+ if (typeof content === 'string') {
419
+ copyTextToClipboard(content);
420
+ } else {
421
+ copyTextToClipboard(
422
+ content
423
+ .map(item => {
424
+ if (item.type === 'text') {
425
+ return item.text;
426
+ }
427
+ })
428
+ .filter(text => !!text)
429
+ .join('\n')
430
+ .trim()
431
+ );
432
+ }
433
+ },
434
+ rmMessage(index) {
435
+ this.messages.splice(index, 1);
436
+ },
437
+ clearChat() {
438
+ this.messages = [];
439
+ this.inputText = '';
440
+ this.isThinking = false;
441
+ localStorage.removeItem('fc_ai_chat');
442
+ },
443
+ refreshSuggestions() {
444
+ const allSuggestions = [
445
+ '生成一个就诊满意度问卷表单',
446
+ '创建一个建议收集表单,包含联系人、联系邮箱、分类和建议内容',
447
+ '追加一个用户信息表单',
448
+ '添加一个标签组件,显示文本为 "Tag"',
449
+ '生成一个Vue组件,实现金额输入框',
450
+ '生成一个js版本的高精度加法',
451
+ '添加一个日期选择器组件,用于选择出生日期',
452
+ '删除商品简介字段',
453
+ '当单选框选择 "选项1" 时,显示输入框组件',
454
+ '设置输入框为必填,并限制长度必须大于13',
455
+ '商品价格字段使用数字输入框组件',
456
+ '给输入类组件补充占位提示文本(placeholder)',
457
+ '添加手机号格式验证',
458
+ '添加自定义验证:确认密码必须与密码一致',
459
+ '将姓名和手机号并排显示在同一行',
460
+ ];
461
+
462
+ if (this.page * 4 < allSuggestions.length) {
463
+ this.page++;
464
+ } else {
465
+ this.page = 1;
466
+ }
467
+ const startIndex = (this.page - 1) * 4;
468
+ const endIndex = startIndex + 4;
469
+
470
+ this.suggestions = allSuggestions.slice(startIndex, endIndex);
471
+ },
472
+ selectSuggestion(suggestion) {
473
+ this.inputText = suggestion;
474
+ this.sendMessage();
475
+ },
476
+ scrollToBottom() {
477
+ const content = this.$refs.chatContent;
478
+ if (content && this.isUserAtBottom) {
479
+ content.scrollTop = content.scrollHeight;
480
+ }
481
+ },
482
+ // 检查用户是否在滚动条底部
483
+ checkIfUserAtBottom() {
484
+ const content = this.$refs.chatContent;
485
+ if (content) {
486
+ const isAtBottom = content.scrollTop + content.clientHeight >= content.scrollHeight - 20;
487
+ this.isUserAtBottom = isAtBottom;
488
+ }
489
+ },
490
+ // 处理滚动事件
491
+ handleScroll() {
492
+ this.checkIfUserAtBottom();
493
+ },
494
+ getHistory() {
495
+ const data = localStorage.getItem('fc_ai_chat');
496
+ if (data) {
497
+ this.messages = JSON.parse(data) || [];
498
+ }
499
+ },
500
+ saveHistory() {
501
+ localStorage.setItem('fc_ai_chat', JSON.stringify(this.messages));
502
+ },
503
+ },
504
+ created() {
505
+ this.getHistory();
506
+ this.refreshSuggestions();
507
+ },
508
+ mounted() {
509
+ this.$nextTick(() => {
510
+ this.scrollToBottom();
511
+ // 添加滚动事件监听
512
+ const content = this.$refs.chatContent;
513
+ if (content) {
514
+ content.addEventListener('scroll', this.handleScroll);
515
+ }
516
+ });
517
+ },
518
+
519
+ beforeUnmount() {
520
+ // 清理滚动事件监听
521
+ const content = this.$refs.chatContent;
522
+ if (content) {
523
+ content.removeEventListener('scroll', this.handleScroll);
524
+ }
525
+ // 清理全局方法
526
+ delete window._fd_copyCode;
527
+ delete window._fd_importCode;
528
+ },
529
+
530
+ updated() {
531
+ // 只在用户位于底部时才自动滚动
532
+ this.$nextTick(() => {
533
+ this.scrollToBottom();
534
+ });
535
+ },
536
+ };
537
+ </script>
538
+
539
+ <style scoped>
540
+ ._fd-ai-panel {
541
+ display: flex;
542
+ flex-direction: column;
543
+ height: 100%;
544
+ background: #ffffff;
545
+ border-radius: 8px;
546
+ overflow: hidden;
547
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
548
+ }
549
+
550
+ /* 头部样式 */
551
+ ._fd-ai-header {
552
+ display: flex;
553
+ align-items: center;
554
+ justify-content: space-between;
555
+ padding: 0 20px;
556
+ height: 40px;
557
+ border-bottom: 1px solid #ececec;
558
+ background: #ffffff;
559
+ }
560
+
561
+ ._fd-ai-title {
562
+ display: flex;
563
+ align-items: center;
564
+ gap: 8px;
565
+ }
566
+
567
+ ._fd-ai-name {
568
+ font-weight: 600;
569
+ color: #262626;
570
+ font-size: 14px;
571
+ }
572
+
573
+ ._fd-ai-actions {
574
+ display: flex;
575
+ gap: 8px;
576
+ }
577
+
578
+ ._fd-ai-action-btn {
579
+ width: 24px;
580
+ height: 24px;
581
+ border: none;
582
+ background: transparent;
583
+ border-radius: 3px;
584
+ color: #666666;
585
+ cursor: pointer;
586
+ display: flex;
587
+ align-items: center;
588
+ justify-content: center;
589
+ transition: all 0.15s ease;
590
+ opacity: 0.8;
591
+ }
592
+
593
+ ._fd-ai-action-btn:hover {
594
+ background: #f5f5f5;
595
+ color: #262626;
596
+ opacity: 1;
597
+ }
598
+
599
+ /* 内容区域样式 */
600
+ ._fd-ai-content {
601
+ flex: 1;
602
+ overflow-y: auto;
603
+ padding: 0 20px;
604
+ display: flex;
605
+ flex-direction: column;
606
+ gap: 16px;
607
+ min-height: 0;
608
+ }
609
+
610
+ /* 当没有消息时,让内容居中显示 */
611
+ ._fd-ai-content--centered {
612
+ justify-content: center;
613
+ align-items: center;
614
+ }
615
+
616
+ ._fd-ai-welcome {
617
+ text-align: center;
618
+ padding: 20px;
619
+ color: #666666;
620
+ max-width: 600px;
621
+ margin: 0 auto;
622
+ }
623
+
624
+ ._fd-ai-message-avatar,
625
+ ._fd-ai-welcome-icon {
626
+ background-image: url('data:image/png;base64,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');
627
+ background-size: 45px;
628
+ background-repeat: no-repeat;
629
+ background-position: center;
630
+ }
631
+
632
+ ._fd-ai-welcome-icon {
633
+ width: 64px;
634
+ height: 64px;
635
+ margin: 0 auto 20px;
636
+ background-color: #f5f5f5;
637
+ border-radius: 50%;
638
+ display: flex;
639
+ align-items: center;
640
+ justify-content: center;
641
+ }
642
+
643
+ ._fd-ai-welcome h3 {
644
+ margin: 0 0 12px;
645
+ color: #262626;
646
+ font-size: 24px;
647
+ font-weight: 600;
648
+ }
649
+
650
+ ._fd-ai-welcome p {
651
+ margin: 0 0 32px;
652
+ font-size: 16px;
653
+ line-height: 1.6;
654
+ color: #666666;
655
+ }
656
+
657
+ /* 建议问题样式 */
658
+ ._fd-ai-welcome-suggestions {
659
+ text-align: left;
660
+ }
661
+
662
+ ._fd-ai-suggestions-header {
663
+ display: flex;
664
+ align-items: center;
665
+ justify-content: space-between;
666
+ margin-bottom: 16px;
667
+ }
668
+
669
+ ._fd-ai-suggestions-header span {
670
+ font-size: 14px;
671
+ font-weight: 500;
672
+ color: #262626;
673
+ }
674
+
675
+ ._fd-ai-refresh-btn {
676
+ display: flex;
677
+ align-items: center;
678
+ gap: 4px;
679
+ padding: 4px 8px;
680
+ border: none;
681
+ background: transparent;
682
+ color: #aaaaaa;
683
+ font-size: 12px;
684
+ cursor: pointer;
685
+ border-radius: 4px;
686
+ transition: all 0.2s ease;
687
+ }
688
+
689
+ ._fd-ai-refresh-btn:hover {
690
+ background: #f5f5f5;
691
+ color: #262626;
692
+ }
693
+
694
+ ._fd-ai-suggestions-list {
695
+ display: grid;
696
+ grid-template-columns: repeat(2, 1fr);
697
+ gap: 12px;
698
+ }
699
+
700
+ ._fd-ai-suggestion-item {
701
+ padding: 12px 16px;
702
+ border: 1px solid #ececec;
703
+ border-radius: 12px;
704
+ background: #f5f5f5;
705
+ color: #262626;
706
+ font-size: 14px;
707
+ text-align: left;
708
+ cursor: pointer;
709
+ transition: all 0.2s ease;
710
+ line-height: 1.4;
711
+ }
712
+
713
+ ._fd-ai-suggestion-item:hover {
714
+ border-color: #2e73ff;
715
+ background: #ffffff;
716
+ transform: translateY(-1px);
717
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
718
+ }
719
+
720
+ /* 消息样式 */
721
+ ._fd-ai-messages {
722
+ display: flex;
723
+ flex-direction: column;
724
+ margin-top: 16px;
725
+ }
726
+
727
+ ._fd-ai-message {
728
+ display: flex;
729
+ flex-direction: column;
730
+ }
731
+
732
+ ._fd-ai-message-content {
733
+ display: flex;
734
+ gap: 12px;
735
+ align-items: flex-start;
736
+ position: relative;
737
+ }
738
+
739
+ ._fd-ai-message-content:hover ._fd-ai-message-footer {
740
+ display: flex;
741
+ }
742
+
743
+ ._fd-ai-message--user ._fd-ai-message-content,
744
+ ._fd-ai-message--user ._fd-ai-message-actions {
745
+ flex-direction: row-reverse;
746
+ }
747
+
748
+ ._fd-ai-message-avatar {
749
+ width: 32px;
750
+ height: 32px;
751
+ border-radius: 50%;
752
+ background-color: #f5f5f5;
753
+ display: flex;
754
+ align-items: center;
755
+ justify-content: center;
756
+ flex-shrink: 0;
757
+ background-size: 20px;
758
+ }
759
+
760
+ ._fd-ai-message-avatar--error {
761
+ background-color: rgba(255, 46, 46, 0.05);
762
+ }
763
+
764
+ ._fd-ai-message-bubble {
765
+ min-width: 40px;
766
+ min-height: 21px;
767
+ max-width: 70%;
768
+ padding: 12px 16px;
769
+ border-radius: 12px;
770
+ position: relative;
771
+ word-wrap: break-word;
772
+ margin-bottom: 35px;
773
+ }
774
+
775
+ ._fd-ai-message-bubble--user {
776
+ background: #2e73ff;
777
+ color: white;
778
+ border-bottom-right-radius: 4px;
779
+ }
780
+
781
+ ._fd-ai-message-bubble--ai {
782
+ background: #f5f5f5;
783
+ color: #262626;
784
+ border-bottom-left-radius: 4px;
785
+ }
786
+
787
+ ._fd-ai-message-bubble--thinking {
788
+ background: rgba(46, 115, 255, 0.05);
789
+ border: 1px solid #2e73ff;
790
+ border-bottom-left-radius: 4px;
791
+ }
792
+
793
+ ._fd-ai-message-bubble--error {
794
+ background: rgba(255, 46, 46, 0.05);
795
+ color: #ff2e2e;
796
+ border: 1px solid #ff2e2e;
797
+ border-bottom-left-radius: 4px;
798
+ }
799
+
800
+ ._fd-ai-message-text {
801
+ font-size: 14px;
802
+ line-height: 1.5;
803
+ }
804
+
805
+ ._fd-ai-message-text :deep(pre) {
806
+ margin: 0;
807
+ background: transparent;
808
+ padding: 0;
809
+ border-radius: 0;
810
+ overflow: visible;
811
+ }
812
+
813
+ ._fd-ai-message-text :deep(code) {
814
+ background: transparent;
815
+ padding: 0;
816
+ border-radius: 0;
817
+ font-family: inherit;
818
+ font-size: inherit;
819
+ }
820
+
821
+ ._fd-ai-message-actions {
822
+ display: flex;
823
+ gap: 4px;
824
+ }
825
+
826
+ ._fd-ai-message-text--error {
827
+ color: #ff2e2e;
828
+ font-weight: 500;
829
+ }
830
+
831
+ /* 思考中状态 */
832
+ ._fd-ai-thinking-status {
833
+ display: flex;
834
+ align-items: center;
835
+ gap: 8px;
836
+ padding: 8px 12px;
837
+ margin: 8px 0;
838
+ }
839
+
840
+ /* 底部悬浮区域 */
841
+ ._fd-ai-message-footer {
842
+ position: absolute;
843
+ bottom: 6px;
844
+ left: 43px;
845
+ display: none;
846
+ align-items: center;
847
+ gap: 8px;
848
+ }
849
+
850
+ /* AI 消息的 footer 在右边 */
851
+ ._fd-ai-message--user ._fd-ai-message-footer {
852
+ right: 0;
853
+ left: unset;
854
+ }
855
+
856
+ /* 思考指示器 */
857
+ ._fd-ai-thinking-indicator {
858
+ display: flex;
859
+ gap: 3px;
860
+ align-items: center;
861
+ }
862
+
863
+ ._fd-ai-thinking-dot {
864
+ width: 4px;
865
+ height: 4px;
866
+ border-radius: 50%;
867
+ background: #666666;
868
+ animation: thinking-pulse 1.4s ease-in-out infinite both;
869
+ }
870
+
871
+ ._fd-ai-thinking-dot:nth-child(1) {
872
+ animation-delay: -0.32s;
873
+ }
874
+
875
+ ._fd-ai-thinking-dot:nth-child(2) {
876
+ animation-delay: -0.16s;
877
+ }
878
+
879
+ @keyframes thinking-pulse {
880
+ 0%,
881
+ 80%,
882
+ 100% {
883
+ transform: scale(0.8);
884
+ opacity: 0.5;
885
+ }
886
+ 40% {
887
+ transform: scale(1);
888
+ opacity: 1;
889
+ }
890
+ }
891
+
892
+ /* 思考内容 */
893
+
894
+ ._fd-ai-thinking-section {
895
+ margin: 8px 0;
896
+ }
897
+
898
+ ._fd-ai-thinking-steps {
899
+ display: flex;
900
+ flex-direction: column;
901
+ gap: 8px;
902
+ }
903
+
904
+ ._fd-ai-thinking-step {
905
+ display: flex;
906
+ align-items: center;
907
+ gap: 10px;
908
+ padding: 8px 12px;
909
+ margin: 2px 0;
910
+ font-size: 13px;
911
+ color: #aaaaaa;
912
+ border-radius: 8px;
913
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
914
+ position: relative;
915
+ overflow: hidden;
916
+ border: 1px solid #ececec;
917
+ }
918
+
919
+ ._fd-ai-thinking-step--active {
920
+ color: #262626;
921
+ background: #ffffff;
922
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
923
+ transform: translateX(2px);
924
+ border: 0 none;
925
+ }
926
+
927
+ ._fd-ai-thinking-step-text {
928
+ position: relative;
929
+ z-index: 1;
930
+ }
931
+
932
+ ._fd-ai-thinking-step-icon {
933
+ display: flex;
934
+ align-items: center;
935
+ justify-content: center;
936
+ flex-shrink: 0;
937
+ transition: all 0.3s ease;
938
+ position: relative;
939
+ z-index: 1;
940
+ color: #aaaaaa;
941
+ background: #ececec;
942
+ border-radius: 50%;
943
+ width: 20px;
944
+ height: 20px;
945
+ }
946
+
947
+ ._fd-ai-thinking-step-icon--executing {
948
+ color: #2e73ff;
949
+ background: rgba(59, 130, 246, 0.1);
950
+ border-radius: 50%;
951
+ width: 20px;
952
+ height: 20px;
953
+ transform: scale(1.1);
954
+ animation: pulse 1.5s ease-in-out infinite;
955
+ }
956
+
957
+ ._fd-ai-thinking-step-icon--completed {
958
+ color: #00c050;
959
+ background: rgba(34, 197, 94, 0.1);
960
+ border-radius: 50%;
961
+ width: 20px;
962
+ height: 20px;
963
+ }
964
+
965
+ @keyframes pulse {
966
+ 0%,
967
+ 100% {
968
+ transform: scale(1.1);
969
+ opacity: 1;
970
+ }
971
+ 50% {
972
+ transform: scale(1.2);
973
+ opacity: 0.8;
974
+ }
975
+ }
976
+
977
+ ._fd-ai-thinking-step-loading {
978
+ width: 10px;
979
+ height: 10px;
980
+ border: 2px solid #2e73ff;
981
+ border-top: 2px solid transparent;
982
+ border-radius: 50%;
983
+ animation: spin 1s linear infinite;
984
+ }
985
+
986
+ ._fd-ai-thinking-step-pending {
987
+ width: 6px;
988
+ height: 6px;
989
+ border-radius: 50%;
990
+ background: #aaaaaa;
991
+ opacity: 0.5;
992
+ }
993
+
994
+ @keyframes spin {
995
+ 0% {
996
+ transform: rotate(0deg);
997
+ }
998
+ 100% {
999
+ transform: rotate(360deg);
1000
+ }
1001
+ }
1002
+
1003
+ /* 输入区域样式 */
1004
+ ._fd-ai-input {
1005
+ padding: 20px 24px;
1006
+ background: #ffffff;
1007
+ display: flex;
1008
+ flex-direction: column;
1009
+ gap: 16px;
1010
+ position: relative;
1011
+ }
1012
+
1013
+ ._fd-ai-input-container {
1014
+ display: flex;
1015
+ align-items: flex-end;
1016
+ gap: 12px;
1017
+ background: #f5f5f5;
1018
+ border-radius: 15px;
1019
+ padding: 12px 16px;
1020
+ border: 1px solid #ececec;
1021
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
1022
+ position: relative;
1023
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
1024
+ }
1025
+
1026
+ ._fd-ai-input-container:focus-within {
1027
+ border-color: #2e73ff;
1028
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1),
1029
+ 0 4px 12px rgba(0, 0, 0, 0.15);
1030
+ }
1031
+
1032
+ ._fd-ai-input-field {
1033
+ flex: 1;
1034
+ border: none;
1035
+ background: transparent;
1036
+ resize: none;
1037
+ outline: none;
1038
+ font-size: 15px;
1039
+ line-height: 1.6;
1040
+ color: #262626;
1041
+ min-height: 24px;
1042
+ max-height: 120px;
1043
+ font-family: inherit;
1044
+ font-weight: 400;
1045
+ padding: 4px 0;
1046
+ }
1047
+
1048
+ ._fd-ai-input-field::placeholder {
1049
+ color: #aaaaaa;
1050
+ font-weight: 400;
1051
+ }
1052
+
1053
+ ._fd-ai-send-btn {
1054
+ width: 32px;
1055
+ height: 32px;
1056
+ border: none;
1057
+ background: #2e73ff;
1058
+ color: white;
1059
+ border-radius: 50%;
1060
+ cursor: pointer;
1061
+ display: flex;
1062
+ align-items: center;
1063
+ justify-content: center;
1064
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
1065
+ flex-shrink: 0;
1066
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
1067
+ }
1068
+
1069
+ ._fd-ai-send-btn:hover:not(._fd-ai-send-btn--disabled) {
1070
+ transform: translateY(-1px);
1071
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1072
+ }
1073
+
1074
+ ._fd-ai-send-btn--disabled {
1075
+ background: #aaaaaa;
1076
+ cursor: not-allowed;
1077
+ opacity: 0.5;
1078
+ transform: none;
1079
+ box-shadow: none;
1080
+ }
1081
+
1082
+ /* 输入框聚焦时的微动画 */
1083
+ ._fd-ai-input-container:focus-within ._fd-ai-send-btn {
1084
+ transform: scale(1.02);
1085
+ }
1086
+
1087
+ /* 输入框占位符动画 */
1088
+ ._fd-ai-input-field:focus::placeholder {
1089
+ opacity: 0.7;
1090
+ transform: translateY(-1px);
1091
+ transition: all 0.2s ease;
1092
+ }
1093
+
1094
+ /* 滚动条样式 */
1095
+ ._fd-ai-content {
1096
+ scrollbar-width: thin;
1097
+ scrollbar-color: #ececec transparent;
1098
+ }
1099
+
1100
+ ._fd-ai-content::-webkit-scrollbar {
1101
+ width: 6px;
1102
+ }
1103
+
1104
+ ._fd-ai-content::-webkit-scrollbar-track {
1105
+ background: transparent;
1106
+ }
1107
+
1108
+ ._fd-ai-content::-webkit-scrollbar-thumb {
1109
+ background: #ececec;
1110
+ border-radius: 3px;
1111
+ opacity: 0;
1112
+ transition: opacity 0.3s ease;
1113
+ }
1114
+
1115
+ ._fd-ai-content:hover::-webkit-scrollbar-thumb {
1116
+ opacity: 1;
1117
+ }
1118
+
1119
+ ._fd-ai-content::-webkit-scrollbar-thumb:hover {
1120
+ background: #aaaaaa;
1121
+ }
1122
+ </style>