st-comp 0.0.262 → 0.0.266

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 (140) hide show
  1. package/es/ChartLayout.js +4 -4
  2. package/es/CustomFunction.cjs +1 -1
  3. package/es/CustomFunction.js +31 -31
  4. package/es/FactorWarning.cjs +1 -1
  5. package/es/FactorWarning.js +37 -36
  6. package/es/Kline.cjs +1 -1
  7. package/es/Kline.js +839 -1593
  8. package/es/KlineBasic.cjs +1 -1
  9. package/es/KlineBasic.js +1198 -1
  10. package/es/KlineConfig.cjs +1 -1
  11. package/es/KlineConfig.js +89 -89
  12. package/es/KlineNew.cjs +1 -1
  13. package/es/KlineNew.js +17 -17
  14. package/es/KlinePlus.cjs +1 -1
  15. package/es/KlinePlus.js +20 -20
  16. package/es/MonacoEditor.cjs +1 -1
  17. package/es/MonacoEditor.js +25 -25
  18. package/es/Pagination.cjs +1 -1
  19. package/es/Pagination.js +113 -113
  20. package/es/PasswordPrompt.cjs +1 -1
  21. package/es/PasswordPrompt.js +3 -3
  22. package/es/Table.cjs +1 -1
  23. package/es/Table.js +29 -29
  24. package/es/User.cjs +1 -1
  25. package/es/User.js +25 -25
  26. package/es/VarSelectDialog.cjs +1 -1
  27. package/es/VarSelectDialog.js +27 -27
  28. package/es/VarietyAiHelper.cjs +3 -3
  29. package/es/VarietyAiHelper.js +324 -283
  30. package/es/VarietyAutoComplete.cjs +1 -1
  31. package/es/VarietyAutoComplete.js +11 -11
  32. package/es/VarietySearch.cjs +1 -1
  33. package/es/VarietySearch.js +56 -55
  34. package/es/{VarietySelect-faef4799.js → VarietySelect-1ac10ec2.js} +4 -4
  35. package/es/{VarietySelect-5c845562.cjs → VarietySelect-639a8d9a.cjs} +1 -1
  36. package/es/VarietyTextCopy.cjs +1 -1
  37. package/es/VarietyTextCopy.js +13 -13
  38. package/es/VirtualTable.cjs +1 -1
  39. package/es/VirtualTable.js +14 -14
  40. package/es/{_initCloneObject-009eedf8.js → _initCloneObject-6b8c230e.js} +7 -7
  41. package/es/{_initCloneObject-2b82e9f7.cjs → _initCloneObject-9eddcb0c.cjs} +1 -1
  42. package/es/aiTools-6c9c58bd.cjs +4 -0
  43. package/es/aiTools-ab67ffa0.js +149 -0
  44. package/es/aiTools.js +92 -1
  45. package/es/{base-7ff209fc.js → base-29f73b05.js} +40 -40
  46. package/es/{castArray-b9f1609f.js → castArray-609a313e.js} +1 -1
  47. package/es/{config-provider-071b8bec.js → config-provider-56193d47.js} +6 -6
  48. package/es/{config-provider-2ae47cc8.cjs → config-provider-ea286661.cjs} +1 -1
  49. package/es/{debounce-23c2f7e1.js → debounce-a09ce9a3.js} +1 -1
  50. package/es/{dropdown-9941f401.js → dropdown-4e541d60.js} +4 -4
  51. package/es/{dropdown-eba9eaf5.cjs → dropdown-f30148cc.cjs} +1 -1
  52. package/es/{el-autocomplete-b59eb529.cjs → el-autocomplete-3949e68c.cjs} +1 -1
  53. package/es/{el-autocomplete-43f205b0.js → el-autocomplete-4b521c83.js} +17 -17
  54. package/es/{el-button-7f58608d.js → el-button-4f201000.js} +33 -33
  55. package/es/{el-button-974ff9e9.cjs → el-button-dd065de0.cjs} +1 -1
  56. package/es/{el-checkbox-08185353.cjs → el-checkbox-02ce40f0.cjs} +1 -1
  57. package/es/{el-checkbox-40664b27.js → el-checkbox-33cb73db.js} +17 -17
  58. package/es/el-checkbox-group-0ea8fbf8.cjs +1 -0
  59. package/es/el-checkbox-group-4ed993c7.js +1 -0
  60. package/es/{el-dialog-2f10a2f4.js → el-dialog-1939f7c5.js} +15 -15
  61. package/es/{el-dialog-1b185570.cjs → el-dialog-6501a151.cjs} +1 -1
  62. package/es/{el-divider-4e66c835.js → el-divider-523e5874.js} +1 -1
  63. package/es/{el-empty-e0d826de.js → el-empty-f5a1607a.js} +5 -5
  64. package/es/{el-form-item-c997b4fa.cjs → el-form-item-316b35d8.cjs} +1 -1
  65. package/es/{el-form-item-2b68cdf5.js → el-form-item-ea11211d.js} +36 -36
  66. package/es/{el-input-fa18ef84.cjs → el-input-094afbe2.cjs} +1 -1
  67. package/es/{el-input-31a818d4.js → el-input-58786d42.js} +43 -43
  68. package/es/{el-checkbox-group-f775e73f.cjs → el-input-number-a609a5e3.cjs} +1 -1
  69. package/es/{el-checkbox-group-3ddad0d7.js → el-input-number-a7971697.js} +30 -30
  70. package/es/{el-loading-969a79ca.cjs → el-loading-d171ce64.cjs} +1 -1
  71. package/es/{el-loading-3e350f5e.js → el-loading-f3815921.js} +5 -5
  72. package/es/el-menu-item-3d1e0aff.cjs +1 -0
  73. package/es/el-menu-item-a166b997.js +769 -0
  74. package/es/{el-message-box-31e0aa98.cjs → el-message-box-b8ffcf34.cjs} +1 -1
  75. package/es/{el-message-box-a3aa9b89.js → el-message-box-f2b667e9.js} +11 -11
  76. package/es/{el-overlay-4e9a99e0.js → el-overlay-1dfe3675.js} +59 -59
  77. package/es/{el-overlay-ea65cb05.cjs → el-overlay-a94f8a98.cjs} +1 -1
  78. package/es/{el-popconfirm-9e232436.cjs → el-popconfirm-70e2849c.cjs} +1 -1
  79. package/es/{el-popconfirm-c2896741.js → el-popconfirm-ca31ceea.js} +12 -12
  80. package/es/{el-popper-2d3914e4.cjs → el-popper-ce575c12.cjs} +1 -1
  81. package/es/{el-popper-003b3af7.js → el-popper-db6c599f.js} +177 -177
  82. package/es/{el-segmented-140ac042.cjs → el-segmented-4d50b63f.cjs} +1 -1
  83. package/es/{el-segmented-4ae54e6a.js → el-segmented-741f2252.js} +8 -8
  84. package/es/{el-select-19ee0e79.js → el-select-086bcb6c.js} +65 -65
  85. package/es/{el-select-a11f33e8.cjs → el-select-2ebc8380.cjs} +1 -1
  86. package/es/{el-table-column-98570a4d.cjs → el-table-column-2f07fbdb.cjs} +1 -1
  87. package/es/{el-table-column-3be856d7.js → el-table-column-8a15378a.js} +16 -16
  88. package/es/{el-tag-19cc5b59.js → el-tag-65a99986.js} +14 -14
  89. package/es/{el-tag-6d8e653e.cjs → el-tag-f48b1190.cjs} +1 -1
  90. package/es/{el-text-a18106cb.cjs → el-text-33359f44.cjs} +1 -1
  91. package/es/{el-text-9a7a4f0f.js → el-text-cbb693f2.js} +2 -2
  92. package/es/{index-99abf4ff.js → index-072c4a65.js} +17 -17
  93. package/es/{index-299ee017.cjs → index-0f767104.cjs} +1 -1
  94. package/es/{index-eb99b188.cjs → index-18565979.cjs} +1 -1
  95. package/es/{index-19ac550e.js → index-1955f23d.js} +49 -49
  96. package/es/{index-6917da9d.js → index-1e91e986.js} +43 -43
  97. package/es/{index-6ca95c8a.cjs → index-21e8d2bc.cjs} +1 -1
  98. package/es/{index-33f80550.cjs → index-3792552a.cjs} +1 -1
  99. package/es/{index-7dce9f59.cjs → index-4f900527.cjs} +1 -1
  100. package/es/{index-308aab33.js → index-57d82da0.js} +34 -34
  101. package/es/{index-e8eeea22.cjs → index-5b546f7d.cjs} +1 -1
  102. package/es/{index-808db9b4.js → index-7db02db7.js} +11 -11
  103. package/es/{index-37b8d3c6.cjs → index-7eb88616.cjs} +1 -1
  104. package/es/{index-d898531c.js → index-8391a3df.js} +37 -37
  105. package/es/{index-6b99def3.cjs → index-8439d2f9.cjs} +1 -1
  106. package/es/{index-40f05e2c.cjs → index-8b055879.cjs} +2 -2
  107. package/es/{index-f3799536.js → index-a4e252a0.js} +2 -2
  108. package/es/{index-b56d81e8.js → index-b51915a2.js} +3 -3
  109. package/es/{index-28e03bad.cjs → index-bf98dd03.cjs} +1 -1
  110. package/es/{index-e17987ac.js → index-ca91ac68.js} +83 -68
  111. package/es/{index-41dd5a1c.js → index-d857270a.js} +10 -10
  112. package/es/{index-f402d1da.js → index-de24705f.js} +1 -1
  113. package/es/{index-6d530e54.js → index-ff26b1a6.js} +13 -13
  114. package/es/{python-02c3937a.cjs → python-5b5c9c58.cjs} +1 -1
  115. package/es/{python-be79c7c1.js → python-ad9239f9.js} +25 -25
  116. package/es/raf-744cf95a.js +6 -0
  117. package/es/{scroll-8642151c.js → scroll-6799bafc.js} +5 -5
  118. package/es/style.css +1 -1
  119. package/es/{use-form-common-props-d3ed62c6.cjs → use-form-common-props-00ec25ac.cjs} +1 -1
  120. package/es/{use-form-common-props-d170ccfd.js → use-form-common-props-c14990b9.js} +65 -65
  121. package/es/{use-global-config-c80f33a4.cjs → use-global-config-28efb416.cjs} +1 -1
  122. package/es/{use-global-config-74436b92.js → use-global-config-a01b5ce1.js} +14 -14
  123. package/es/{validator-07160325.cjs → validator-119fdaf4.cjs} +1 -1
  124. package/es/{validator-672bad4a.js → validator-65de1caf.js} +1 -1
  125. package/es/{vnode-6d2615f0.js → vnode-a83e6de8.js} +1 -1
  126. package/es/{zh-cn-6a0f844c.cjs → zh-cn-9fb29a39.cjs} +1 -1
  127. package/es/{zh-cn-ed10eeb1.js → zh-cn-c1c28e70.js} +2 -2
  128. package/lib/aiTools.js +92 -1
  129. package/lib/bundle.js +1 -1
  130. package/lib/bundle.umd.cjs +182 -181
  131. package/lib/{index-c3ddbee7.js → index-53b3465c.js} +18966 -18852
  132. package/lib/{python-da8fd227.js → python-4a270481.js} +1 -1
  133. package/lib/style.css +1 -1
  134. package/package.json +1 -1
  135. package/packages/KlineBasic/index.vue +1 -1
  136. package/packages/VarietyAiHelper/index.vue +271 -290
  137. package/public/aiTools.js +92 -1
  138. package/es/aiTools-2e1f92d2.cjs +0 -3
  139. package/es/aiTools-faa0a14d.js +0 -90
  140. package/es/raf-ba3dfafe.js +0 -6
@@ -3,9 +3,10 @@ import dayjs from "dayjs";
3
3
  import { ElMessage } from "element-plus";
4
4
  import { getUserData } from "st-func";
5
5
  import { inject, ref, nextTick, watch, reactive, onMounted } from "vue";
6
- import { sendToBaiLianAppStreaming } from "../../public/aiTools";
7
- import { UserFilled, Service, Promotion } from "@element-plus/icons-vue";
6
+ import { sendToBaiLianWorkflowStreaming } from "../../public/aiTools";
7
+ import { UserFilled, Service, Promotion, Refresh, SuccessFilled } from "@element-plus/icons-vue";
8
8
 
9
+ // ==================== 基础配置 ====================
9
10
  const stConfig = inject("stConfig");
10
11
  const userData = reactive(getUserData());
11
12
  const visible = ref(false);
@@ -13,41 +14,43 @@ const emit = defineEmits(["callBack"]);
13
14
  const props = defineProps({
14
15
  defaultMessage: {
15
16
  type: String,
16
- default: "你好呀!我是你的品种池AI助手,会根据您的自然语言去进行品种的条件查询\n\n示例: \n帮我查A股科创板下品种转价差上证50并且总市值大于1千万, 最近一次红箱的开盘价大于前5根K线的最高价, 并按照总市值升序进行排序",
17
+ default:
18
+ "你好呀!我是你的品种池AI助手,会根据您的自然语言去进行品种的条件查询\n\n示例: \n帮我查A股科创板下品种转价差上证50并且总市值大于1千万, 最近一次红箱的开盘价大于前5根K线的最高价, 并按照总市值升序进行排序",
17
19
  },
18
20
  });
19
21
 
20
- // 自定义标签数据
21
- const tagMap = ref();
22
+ // ==================== 响应式数据 ====================
23
+ const tagMap = ref(); // 自定义标签映射表
24
+ const isSending = ref(false); // 是否正在发送消息
25
+ const isThinking = ref(false); // AI是否正在思考(首包到达前)
26
+ const nodeProgressList = ref([]); // 工作流节点执行进度列表
27
+ const showFinalResult = ref(false); // 是否展示最终结果
22
28
 
23
- // loading状态
24
- const isSending = ref(false);
25
- const isThinking = ref(false);
26
-
27
- // 消息队列
29
+ // ==================== 消息队列 ====================
28
30
  const messageListRef = ref(null);
29
31
  const messageList = ref([
30
32
  {
31
- role: "assistant", // AI-assistant, 用户-user
33
+ role: "assistant",
32
34
  content: props.defaultMessage,
33
35
  userContent: null,
34
- showFeedback: false, // 是否展示反馈按钮(默认信息不用展示)
35
- hasFeedback: false, // 是否已进行过反馈
36
- createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), // 消息发起时间
37
- resTime: 0, // 响应总耗时
38
- firstPackageTime: 0, // 首包响应耗时
36
+ showFeedback: false,
37
+ hasFeedback: false,
38
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
39
+ resTime: 0,
40
+ firstPackageTime: 0,
39
41
  },
40
42
  ]);
41
43
 
42
- // 用户输入
43
- const userInput = ref("");
44
+ const userInput = ref(""); // 用户输入内容
44
45
 
45
- // 反馈弹窗
46
+ // ==================== 反馈弹窗 ====================
46
47
  const feedbackDialogVisible = ref(false);
47
48
  const feedbackContent = ref("");
48
- const feedbackMessageIndex = ref({}); // 当前点击要反馈的message的索引
49
+ const feedbackMessageIndex = ref({});
49
50
 
50
- // 辅助函数:判断是否可JSON序列化
51
+ // ==================== 工具函数 ====================
52
+
53
+ // 判断字符串是否可解析为 JSON 对象
51
54
  const isJSONSerializable = (str) => {
52
55
  if (typeof str !== "string") return false;
53
56
  try {
@@ -57,25 +60,23 @@ const isJSONSerializable = (str) => {
57
60
  return false;
58
61
  }
59
62
  };
60
- // 辅助函数:解析并格式化JSON内容
63
+
64
+ // 将 JSON 字符串解析为展示数据(parsedConditions + 其他参数)
61
65
  const formatJSONContent = (content) => {
62
66
  if (!isJSONSerializable(content)) return null;
63
67
  try {
64
68
  const jsonData = JSON.parse(content);
65
69
  const { parsedConditions, ...webParams } = jsonData;
66
- return {
67
- parsedConditions: parsedConditions,
68
- webParams,
69
- };
70
+ return { parsedConditions, webParams };
70
71
  } catch {
71
72
  return null;
72
73
  }
73
74
  };
74
- // 辅助函数:渲染JSON内容为HTML
75
+
76
+ // 将解析后的数据渲染为 HTML 字符串
75
77
  const renderJSONContent = (jsonData) => {
76
78
  if (!jsonData) return "";
77
79
  let html = "";
78
- // 渲染 parsedConditions
79
80
  if (jsonData.parsedConditions?.length) {
80
81
  html += '<div class="parsed-conditions">';
81
82
  jsonData.parsedConditions.forEach((text) => {
@@ -83,20 +84,17 @@ const renderJSONContent = (jsonData) => {
83
84
  });
84
85
  html += "</div>";
85
86
  }
86
- // 渲染 webParams
87
- // html += '<div class="web-params">';
88
- // html += `<div>${JSON.stringify(jsonData.webParams)}</div>`;
89
- // html += "</div>";
90
87
  return html;
91
88
  };
92
89
 
93
- // 反馈弹窗相关函数处理
90
+ // ==================== 反馈处理 ====================
91
+
92
+ // 处理用户反馈操作(满意 / 不满意 / 默认记录)
94
93
  const handleFeedbackAction = async (action, messageIndex) => {
95
94
  switch (action) {
96
- // 窗口: 提交(满意)
97
95
  case "satisfied": {
98
96
  const message = messageList.value[messageIndex];
99
- const params = {
97
+ await stConfig.request.post("/alarm/deliversign/addVarietyAiHelperLog", {
100
98
  userName: userData.username,
101
99
  userContent: message.userContent,
102
100
  aiContent: message.content,
@@ -104,25 +102,21 @@ const handleFeedbackAction = async (action, messageIndex) => {
104
102
  createTime: message.createTime,
105
103
  resTime: message.resTime,
106
104
  firstPackageTime: message.firstPackageTime,
107
-
108
- isSolved: 1, // 是否解决
109
- logOrigin: 1, // 日志来源
110
- };
111
- await stConfig.request.post("/alarm/deliversign/addVarietyAiHelperLog", params);
105
+ isSolved: 1,
106
+ logOrigin: 1,
107
+ });
112
108
  ElMessage.success("感谢您的评价!");
113
109
  messageList.value[messageIndex].hasFeedback = true;
114
110
  break;
115
111
  }
116
- // 窗口: 打开
117
112
  case "open": {
118
113
  feedbackMessageIndex.value = messageIndex;
119
114
  feedbackDialogVisible.value = true;
120
115
  break;
121
116
  }
122
- // 窗口: 提交(不满意)
123
117
  case "unsatisfied": {
124
118
  const message = messageList.value[messageIndex];
125
- const params = {
119
+ await stConfig.request.post("/alarm/deliversign/addVarietyAiHelperLog", {
126
120
  userName: userData.username,
127
121
  userContent: message.userContent,
128
122
  userSuggestion: feedbackContent.value,
@@ -131,20 +125,17 @@ const handleFeedbackAction = async (action, messageIndex) => {
131
125
  resTime: message.resTime,
132
126
  createTime: message.createTime,
133
127
  firstPackageTime: message.firstPackageTime,
134
-
135
- isSolved: 0, // 是否解决
136
- logOrigin: 1, // 日志来源
137
- };
138
- await stConfig.request.post("/alarm/deliversign/addVarietyAiHelperLog", params);
128
+ isSolved: 0,
129
+ logOrigin: 1,
130
+ });
139
131
  ElMessage.success("感谢您的反馈!我们将持续跟踪并进行优化");
140
132
  feedbackDialogVisible.value = false;
141
133
  messageList.value[messageIndex].hasFeedback = true;
142
134
  break;
143
135
  }
144
- // 自动提交记录跟踪日志
145
136
  case "default": {
146
137
  const message = messageList.value[messageIndex];
147
- const params = {
138
+ await stConfig.request.post("/alarm/deliversign/addVarietyAiHelperLog", {
148
139
  userName: userData.username,
149
140
  userContent: message.userContent,
150
141
  userSuggestion: feedbackContent.value,
@@ -153,21 +144,28 @@ const handleFeedbackAction = async (action, messageIndex) => {
153
144
  resTime: message.resTime,
154
145
  createTime: message.createTime,
155
146
  firstPackageTime: message.firstPackageTime,
156
-
157
- isSolved: 1, // 是否解决
158
- logOrigin: 1, // 日志来源
159
- };
160
- await stConfig.request.post("/alarm/deliversign/addVarietyAiHelperLog", params);
147
+ isSolved: 1,
148
+ logOrigin: 1,
149
+ });
161
150
  }
162
151
  }
163
152
  };
164
- // 发送消息
153
+
154
+ // ==================== 消息发送 ====================
155
+
156
+ // ==================== 消息发送 ====================
157
+
158
+ // 发送消息到工作流
165
159
  const sendMessage = async () => {
166
160
  const content = userInput.value.trim();
167
161
  if (!content) return ElMessage.warning("请输入消息内容");
168
162
  if (isSending.value) return;
169
163
 
170
- // 记录用户消息
164
+ // 重置状态
165
+ nodeProgressList.value = [];
166
+ showFinalResult.value = false;
167
+
168
+ // 添加用户消息
171
169
  messageList.value.push({
172
170
  role: "user",
173
171
  content: content,
@@ -176,7 +174,7 @@ const sendMessage = async () => {
176
174
  userInput.value = "";
177
175
  await scrollToBottom();
178
176
 
179
- // 创建AI消息占位符,记录对应的用户输入
177
+ // 添加 AI 占位消息
180
178
  messageList.value.push({
181
179
  role: "assistant",
182
180
  content: "",
@@ -194,23 +192,21 @@ const sendMessage = async () => {
194
192
 
195
193
  let fullResponse = "";
196
194
  let resTime = new Date().getTime();
195
+ let firstNodeReceived = false; // 标记是否已收到首个节点推送
196
+
197
197
  try {
198
- const appId = "9e54d112acfe4531bd1fc4fee8827fef";
198
+ const appId = "1977602b357e4dab9d0b74899d5323b3";
199
199
  const apiKey = "sk-d995eb26a4334bdeb2ccb4cbfaf51de8";
200
- await sendToBaiLianAppStreaming({
200
+ await sendToBaiLianWorkflowStreaming({
201
201
  appId,
202
202
  apiKey,
203
203
  value: content,
204
204
  callback: (type, data) => {
205
- // 百炼应用错误
205
+ // 工作流返回错误
206
206
  if (type === "error") {
207
207
  isThinking.value = false;
208
208
  isSending.value = false;
209
-
210
- // 移除占位的AI消息
211
209
  messageList.value.pop();
212
-
213
- // 添加错误提示消息
214
210
  messageList.value.push({
215
211
  role: "assistant",
216
212
  userContent: content,
@@ -225,42 +221,58 @@ const sendMessage = async () => {
225
221
  scrollToBottom();
226
222
  return;
227
223
  }
228
- // 流式输出(正常)
229
- else if (type === "message") {
230
- fullResponse += data;
231
- // 直接更新最后一条AI消息
224
+
225
+ // 节点执行进度更新
226
+ if (type === "node") {
227
+ // 首个节点推送时记录首包响应时间
228
+ if (!firstNodeReceived) {
229
+ firstNodeReceived = true;
230
+ const lastMessage = messageList.value[messageList.value.length - 1];
231
+ if (lastMessage && lastMessage.role === "assistant" && lastMessage.firstPackageTime === 0) {
232
+ lastMessage.firstPackageTime = new Date().getTime() - resTime;
233
+ }
234
+ }
235
+
236
+ const existing = nodeProgressList.value.find((n) => n.name === data.name);
237
+ if (existing) {
238
+ existing.status = data.status;
239
+ } else {
240
+ nodeProgressList.value.push({ name: data.name, status: data.status });
241
+ }
242
+ scrollToBottom();
243
+ return;
244
+ }
245
+
246
+ // 最终结果(工作流结束时一次性推送)
247
+ if (type === "message") {
248
+ fullResponse = data;
232
249
  const lastMessage = messageList.value[messageList.value.length - 1];
233
250
  if (lastMessage && lastMessage.role === "assistant") {
234
251
  lastMessage.content = fullResponse;
235
252
  scrollToBottom();
236
253
  }
237
- // 记录首包响应耗时
238
- if (lastMessage.firstPackageTime === 0) {
239
- lastMessage.firstPackageTime = new Date().getTime() - resTime;
240
- }
241
254
  }
242
- // 流式输出(完毕)
243
- else if (type === "finish") {
255
+
256
+ // 流式结束,展示反馈按钮并触发回调
257
+ if (type === "finish") {
244
258
  isThinking.value = false;
245
259
  isSending.value = false;
260
+ showFinalResult.value = true;
246
261
  const lastMessage = messageList.value[messageList.value.length - 1];
247
262
 
248
- // 显示反馈按钮
249
263
  if (lastMessage && lastMessage.role === "assistant") {
250
264
  lastMessage.showFeedback = true;
251
265
  lastMessage.resTime = new Date().getTime() - resTime;
252
266
  handleFeedbackAction("default", messageList.value.length - 1);
253
267
  }
254
268
 
255
- // 触发回调
256
- console.log(fullResponse);
269
+ // 解析最终结果并回调父组件
257
270
  try {
258
271
  const jsonResponse = JSON.parse(fullResponse);
259
- // 切割处理: parsedConditions, 这个字段仅做AI提炼展示使用
260
272
  if (jsonResponse.parsedConditions) {
261
273
  delete jsonResponse.parsedConditions;
262
274
  }
263
- // 切割处理: customTagNames, 这个字段需要被转换成customTag
275
+ // customTagNames 转换为 customTag
264
276
  if (jsonResponse.customTagNames?.length) {
265
277
  const customTag = jsonResponse.customTagNames.reduce((result, item) => {
266
278
  const id = tagMap.value[item];
@@ -278,10 +290,9 @@ const sendMessage = async () => {
278
290
  },
279
291
  });
280
292
  } catch (error) {
293
+ // 网络异常处理
281
294
  ElMessage.error(`AI响应异常: ${error}`);
282
- // 移除占位的AI消息
283
295
  messageList.value.pop();
284
- // 添加错误提示消息
285
296
  messageList.value.push({
286
297
  role: "assistant",
287
298
  userContent: content,
@@ -299,78 +310,83 @@ const sendMessage = async () => {
299
310
  }
300
311
  };
301
312
 
302
- // 辅助函数: Ctrl+Enter 换行,Enter 发送
313
+ // ==================== 键盘事件 ====================
314
+
315
+ // Enter 发送,Ctrl+Enter 换行
303
316
  const handleKeydown = (event) => {
304
317
  if (event.key === "Enter") {
305
318
  if (event.ctrlKey) {
306
- // Ctrl + Enter: 插入换行符
307
319
  event.preventDefault();
308
320
  const textarea = event.target;
309
321
  const start = textarea.selectionStart;
310
322
  const end = textarea.selectionEnd;
311
323
  userInput.value = userInput.value.substring(0, start) + "\n" + userInput.value.substring(end);
312
- // 将光标移动到新插入的换行符之后
313
324
  nextTick(() => {
314
325
  textarea.selectionStart = textarea.selectionEnd = start + 1;
315
326
  });
316
327
  } else {
317
- // 单独的 Enter: 发送消息
318
328
  event.preventDefault();
319
329
  sendMessage();
320
330
  }
321
331
  }
322
332
  };
323
- // 辅助函数: 滚动到底部
333
+
334
+ // ==================== 滚动 & 初始化 ====================
335
+
336
+ // 自动滚动到消息列表底部
324
337
  const scrollToBottom = async () => {
325
338
  await nextTick();
326
339
  if (messageListRef.value) {
327
340
  messageListRef.value.scrollTop = messageListRef.value.scrollHeight;
328
341
  }
329
342
  };
330
- // 获取全部标签
343
+
344
+ // 获取全部标签映射
331
345
  const getTotalTagMap = async () => {
332
- const res = await Promise.all([stConfig.request.post("/alarm/deliversign/findTagsByUserId"), stConfig.request.post("/alarm/deliversign/findSystemTagsByTagName")]);
346
+ const res = await Promise.all([
347
+ stConfig.request.post("/alarm/deliversign/findTagsByUserId"),
348
+ stConfig.request.post("/alarm/deliversign/findSystemTagsByTagName"),
349
+ ]);
333
350
  tagMap.value = res.reduce((result, item) => {
334
351
  return {
335
352
  ...result,
336
353
  ...item.body?.reduce((cR, cI) => {
337
- return {
338
- ...cR,
339
- [cI.tagName]: cI.id,
340
- };
354
+ return { ...cR, [cI.tagName]: cI.id };
341
355
  }, {}),
342
356
  };
343
357
  }, {});
344
358
  };
359
+
345
360
  onMounted(() => {
346
361
  getTotalTagMap();
347
362
  });
348
- // 监视消息队列 => 自动滚动
363
+
364
+ // 监听消息队列变化,自动滚动
349
365
  watch(
350
366
  () => messageList.value,
351
- () => {
352
- scrollToBottom();
353
- },
367
+ () => { scrollToBottom(); },
354
368
  { deep: true },
355
369
  );
356
- // 监视不满意的反馈详情窗口 => 关闭自动清空数据
370
+
371
+ // 关闭反馈弹窗时清空内容
357
372
  watch(
358
373
  () => feedbackDialogVisible.value,
359
374
  (newValue) => {
360
375
  if (!newValue) feedbackContent.value = "";
361
376
  },
362
377
  );
378
+
379
+ // 暴露 open 方法供父组件调用
363
380
  defineExpose({
364
381
  open: () => {
365
382
  visible.value = true;
366
- nextTick(() => {
367
- scrollToBottom();
368
- });
383
+ nextTick(() => { scrollToBottom(); });
369
384
  },
370
385
  });
371
386
  </script>
372
387
 
373
388
  <template>
389
+ <!-- ==================== 主对话框 ==================== -->
374
390
  <el-dialog
375
391
  class="ai-dialog"
376
392
  v-model="visible"
@@ -383,55 +399,65 @@ defineExpose({
383
399
  :modal-penetrable="true"
384
400
  >
385
401
  <div class="ai-dialog-body">
386
- <div
387
- ref="messageListRef"
388
- class="message-list"
389
- >
402
+ <!-- 消息列表 -->
403
+ <div ref="messageListRef" class="message-list">
390
404
  <div
391
405
  v-for="(message, index) in messageList"
392
406
  :key="index"
393
407
  :class="message.role"
394
408
  class="message-item"
395
409
  >
396
- <template v-if="message.content">
397
- <!-- 消息头像 -->
410
+ <!-- 有内容 或 是最后一条AI消息且有节点进度时,渲染消息气泡 -->
411
+ <template v-if="message.content || (index === messageList.length - 1 && message.role === 'assistant' && nodeProgressList.length > 0)">
412
+ <!-- 头像 -->
398
413
  <div class="avatar">
399
- <el-avatar
400
- :size="32"
401
- :icon="message.role === 'user' ? UserFilled : Service"
402
- />
414
+ <el-avatar :size="32" :icon="message.role === 'user' ? UserFilled : Service" />
403
415
  </div>
416
+
404
417
  <div class="message-content">
405
- <!-- 判断是否为可JSON序列化的数据 -->
406
- <div
407
- v-if="isJSONSerializable(message.content)"
408
- class="message-json"
409
- v-html="renderJSONContent(formatJSONContent(message.content))"
410
- ></div>
411
- <!-- 普通文本展示 -->
418
+ <!-- 节点执行进度(仅最新AI消息 & 未完成时展示) -->
412
419
  <div
413
- v-else
414
- class="message-text"
420
+ v-if="index === messageList.length - 1 && message.role === 'assistant' && nodeProgressList.length > 0 && !showFinalResult"
421
+ class="node-progress"
415
422
  >
416
- {{ message.content }}
423
+ <!-- 已完成/执行中节点 -->
424
+ <div v-for="node in nodeProgressList" :key="node.name" class="node-progress-item">
425
+ <span class="node-status-icon">
426
+ <el-icon v-if="node.status === 'success'" class="node-icon done"><SuccessFilled /></el-icon>
427
+ <el-icon v-else class="node-icon running"><Refresh /></el-icon>
428
+ </span>
429
+ <span class="node-name">{{ node.name }}</span>
430
+ </div>
431
+ <!-- 所有节点完成,等待下一步 -->
432
+ <div
433
+ v-if="nodeProgressList.length > 0 && nodeProgressList.every((n) => n.status === 'success') && !showFinalResult"
434
+ class="node-progress-item next-step"
435
+ >
436
+ <span class="node-status-icon">
437
+ <el-icon class="node-icon waiting"><Refresh /></el-icon>
438
+ </span>
439
+ <span class="node-name">正在执行下一步...</span>
440
+ </div>
417
441
  </div>
442
+
443
+ <!-- JSON 结果渲染 -->
444
+ <div v-if="isJSONSerializable(message.content)" class="message-json" v-html="renderJSONContent(formatJSONContent(message.content))"></div>
445
+ <!-- 普通文本 -->
446
+ <div v-else-if="message.content" class="message-text">{{ message.content }}</div>
447
+
448
+ <!-- 消息时间 -->
418
449
  <div class="message-createTime">{{ message.createTime }}</div>
419
- <!-- 反馈按钮(仅AI侧展示) -->
450
+
451
+ <!-- 反馈按钮(仅AI消息) -->
420
452
  <template v-if="message.role === 'assistant'">
421
453
  <template v-if="message.showFeedback && !message.hasFeedback">
422
454
  <div class="message-createTime">请问对本轮查询结果是否满意?</div>
423
455
  <div class="feedback-buttons">
424
- <button
425
- class="feedback-btn satisfied-btn"
426
- @click="handleFeedbackAction('satisfied', index)"
427
- >
456
+ <button class="feedback-btn satisfied-btn" @click="handleFeedbackAction('satisfied', index)">
428
457
  <span class="btn-emoji">👍</span>
429
458
  <span class="btn-text">满意</span>
430
459
  </button>
431
- <button
432
- class="feedback-btn unsatisfied-btn"
433
- @click="handleFeedbackAction('open', index)"
434
- >
460
+ <button class="feedback-btn unsatisfied-btn" @click="handleFeedbackAction('open', index)">
435
461
  <span class="btn-emoji">👎</span>
436
462
  <span class="btn-text">不满意</span>
437
463
  </button>
@@ -445,16 +471,13 @@ defineExpose({
445
471
  </template>
446
472
  </div>
447
473
 
448
- <!-- AI首包响应思考Loading -->
474
+ <!-- 首包响应前的思考动画 -->
449
475
  <div
450
- v-if="isThinking && !messageList[messageList.length - 1]?.content"
476
+ v-if="isThinking && nodeProgressList.length === 0 && !messageList[messageList.length - 1]?.content"
451
477
  class="message-item assistant"
452
478
  >
453
479
  <div class="avatar">
454
- <el-avatar
455
- :size="32"
456
- :icon="Service"
457
- />
480
+ <el-avatar :size="32" :icon="Service" />
458
481
  </div>
459
482
  <div class="message-content">
460
483
  <div class="typing-indicator">
@@ -466,6 +489,7 @@ defineExpose({
466
489
  </div>
467
490
  </div>
468
491
 
492
+ <!-- 输入区域 -->
469
493
  <div class="input-area">
470
494
  <el-input
471
495
  class="message-input"
@@ -480,14 +504,7 @@ defineExpose({
480
504
  <div class="input-hint">
481
505
  <span>Enter 发送 | Ctrl + Enter 换行</span>
482
506
  </div>
483
- <el-button
484
- class="send-btn"
485
- type="primary"
486
- :icon="Promotion"
487
- :loading="isSending"
488
- @click="sendMessage"
489
- round
490
- >
507
+ <el-button class="send-btn" type="primary" :icon="Promotion" :loading="isSending" @click="sendMessage" round>
491
508
  发送
492
509
  </el-button>
493
510
  </div>
@@ -495,30 +512,17 @@ defineExpose({
495
512
  </div>
496
513
  </el-dialog>
497
514
 
498
- <!-- 窗口: 反馈意见 -->
499
- <el-dialog
500
- v-model="feedbackDialogVisible"
501
- title="📝 反馈意见"
502
- width="400px"
503
- >
515
+ <!-- ==================== 反馈弹窗 ==================== -->
516
+ <el-dialog v-model="feedbackDialogVisible" title="📝 反馈意见" width="400px">
504
517
  <div class="feedback-dialog-content">
505
518
  <div class="feedback-emoji">😟</div>
506
519
  <p class="feedback-tip">很抱歉没能帮到您,请告诉我们哪里需要改进:</p>
507
- <el-input
508
- v-model="feedbackContent"
509
- type="textarea"
510
- :rows="4"
511
- placeholder="[非必填]例如:回答不够准确、查询结果有误、界面体验不佳..."
512
- />
520
+ <el-input v-model="feedbackContent" type="textarea" :rows="4" placeholder="[非必填]例如:回答不够准确、查询结果有误、界面体验不佳..." />
513
521
  </div>
514
522
  <template #footer>
515
523
  <span class="dialog-footer">
516
524
  <el-button @click="feedbackDialogVisible = false">取消</el-button>
517
- <el-button
518
- type="primary"
519
- @click="handleFeedbackAction('unsatisfied', feedbackMessageIndex)"
520
- >提交反馈</el-button
521
- >
525
+ <el-button type="primary" @click="handleFeedbackAction('unsatisfied', feedbackMessageIndex)">提交反馈</el-button>
522
526
  </span>
523
527
  </template>
524
528
  </el-dialog>
@@ -531,45 +535,78 @@ defineExpose({
531
535
  flex-direction: column;
532
536
  height: 480px;
533
537
  background: transparent;
538
+
539
+ // ========== 消息列表 ==========
534
540
  .message-list {
535
541
  flex: 1;
536
542
  overflow-y: auto;
537
543
  padding: 20px 24px;
538
- &::-webkit-scrollbar {
539
- width: 6px;
540
- }
541
- &::-webkit-scrollbar-track {
542
- background: rgba(0, 0, 0, 0.05);
543
- border-radius: 3px;
544
- }
544
+
545
+ // 滚动条
546
+ &::-webkit-scrollbar { width: 6px; }
547
+ &::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.05); border-radius: 3px; }
545
548
  &::-webkit-scrollbar-thumb {
546
549
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
547
550
  border-radius: 3px;
548
-
549
- &:hover {
550
- background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
551
- }
551
+ &:hover { background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); }
552
552
  }
553
- // 消息公共样式
553
+
554
+ // 单条消息
554
555
  .message-item {
555
556
  display: flex;
556
557
  animation: fadeInUp 0.3s ease-out;
557
558
  margin-bottom: 20px;
559
+
558
560
  .avatar {
559
561
  flex-shrink: 0;
560
562
  :deep(.el-avatar) {
561
563
  background: linear-gradient(135deg, #f0f2ff 0%, #e8ecff 100%);
562
564
  color: #667eea;
563
- svg {
564
- width: 18px;
565
- height: 18px;
566
- }
565
+ svg { width: 18px; height: 18px; }
567
566
  }
568
567
  }
568
+
569
569
  .message-content {
570
570
  display: flex;
571
571
  flex-direction: column;
572
572
  max-width: 70%;
573
+
574
+ // 节点进度卡片
575
+ .node-progress {
576
+ padding: 12px 16px;
577
+ border-radius: 12px;
578
+ margin-bottom: 8px;
579
+ border: 1px solid rgba(102, 126, 234, 0.1);
580
+
581
+ .node-progress-item {
582
+ display: flex;
583
+ align-items: center;
584
+ gap: 8px;
585
+ padding: 4px 0;
586
+ font-size: 13px;
587
+ color: #6b7280;
588
+
589
+ .node-status-icon {
590
+ display: flex;
591
+ align-items: center;
592
+ justify-content: center;
593
+ width: 18px;
594
+ flex-shrink: 0;
595
+ }
596
+
597
+ .node-icon {
598
+ font-size: 16px;
599
+ &.done { color: #22c55e; } // 完成:绿色
600
+ &.running { color: #667eea; animation: spin 1.2s linear infinite; } // 执行中:旋转
601
+ &.waiting { color: #d1d5db; animation: spin 1.2s linear infinite; } // 等待中:灰色旋转
602
+ }
603
+
604
+ .node-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
605
+ }
606
+
607
+ .next-step .node-name { color: #9ca3af; }
608
+ }
609
+
573
610
  .message-json,
574
611
  .message-text {
575
612
  padding: 10px 16px;
@@ -578,15 +615,15 @@ defineExpose({
578
615
  word-wrap: break-word;
579
616
  white-space: pre-wrap;
580
617
  }
581
- .message-createTime {
582
- font-size: 11px;
583
- color: #9ca3af;
584
- margin-top: 6px;
585
- }
618
+
619
+ .message-createTime { font-size: 11px; color: #9ca3af; margin-top: 6px; }
620
+
621
+ // 反馈按钮
586
622
  .feedback-buttons {
587
623
  display: flex;
588
624
  gap: 12px;
589
625
  margin-top: 12px;
626
+
590
627
  .feedback-btn {
591
628
  display: flex;
592
629
  align-items: center;
@@ -601,57 +638,35 @@ defineExpose({
601
638
  background: rgba(255, 255, 255, 0.9);
602
639
  backdrop-filter: blur(10px);
603
640
 
604
- .btn-emoji {
605
- font-size: 16px;
606
- transition: transform 0.2s ease;
607
- }
608
- .btn-text {
609
- font-size: 13px;
610
- }
611
- &:hover {
612
- transform: translateY(-2px);
613
- .btn-emoji {
614
- transform: scale(1.1);
615
- }
616
- }
617
- &:active {
618
- transform: translateY(0);
619
- }
641
+ .btn-emoji { font-size: 16px; transition: transform 0.2s ease; }
642
+ .btn-text { font-size: 13px; }
643
+ &:hover { transform: translateY(-2px); .btn-emoji { transform: scale(1.1); } }
644
+ &:active { transform: translateY(0); }
620
645
  }
646
+
621
647
  .satisfied-btn {
622
648
  background: linear-gradient(135deg, #f0f9ff 0%, #e6f7ff 100%);
623
649
  color: #1890ff;
624
650
  border: 1px solid rgba(24, 144, 255, 0.2);
625
651
  box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1);
626
-
627
- &:hover {
628
- background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
629
- border-color: #1890ff;
630
- box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2);
631
- }
652
+ &:hover { background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%); border-color: #1890ff; box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2); }
632
653
  }
654
+
633
655
  .unsatisfied-btn {
634
656
  background: linear-gradient(135deg, #fff1f0 0%, #ffe7e5 100%);
635
657
  color: #ff4d4f;
636
658
  border: 1px solid rgba(255, 77, 79, 0.2);
637
659
  box-shadow: 0 2px 8px rgba(255, 77, 79, 0.1);
638
-
639
- &:hover {
640
- background: linear-gradient(135deg, #ffe7e5 0%, #ffccc7 100%);
641
- border-color: #ff4d4f;
642
- box-shadow: 0 4px 12px rgba(255, 77, 79, 0.2);
643
- }
660
+ &:hover { background: linear-gradient(135deg, #ffe7e5 0%, #ffccc7 100%); border-color: #ff4d4f; box-shadow: 0 4px 12px rgba(255, 77, 79, 0.2); }
644
661
  }
645
662
  }
646
663
  }
647
664
  }
648
- // 用户消息
665
+
666
+ // 用户消息靠右
649
667
  .user {
650
668
  flex-direction: row-reverse;
651
- .avatar {
652
- margin-left: 12px;
653
- margin-right: 0;
654
- }
669
+ .avatar { margin-left: 12px; margin-right: 0; }
655
670
  .message-content {
656
671
  align-items: flex-end;
657
672
  .message-text {
@@ -659,16 +674,13 @@ defineExpose({
659
674
  border-radius: 18px 18px 4px 18px;
660
675
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
661
676
  }
662
- .message-createTime {
663
- text-align: right;
664
- }
677
+ .message-createTime { text-align: right; }
665
678
  }
666
679
  }
667
- // AI消息
680
+
681
+ // AI消息靠左
668
682
  .assistant {
669
- .avatar {
670
- margin-right: 12px;
671
- }
683
+ .avatar { margin-right: 12px; }
672
684
  .message-content {
673
685
  align-items: flex-start;
674
686
  .message-text {
@@ -681,6 +693,8 @@ defineExpose({
681
693
  border-radius: 18px 18px 18px 4px;
682
694
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
683
695
  border: 1px solid rgba(102, 126, 234, 0.2);
696
+
697
+ // 解析条件高亮卡片
684
698
  :deep(.parsed-conditions) {
685
699
  margin-top: 10px;
686
700
  .parsed-conditions-item {
@@ -696,9 +710,12 @@ defineExpose({
696
710
  }
697
711
  }
698
712
  }
713
+
714
+ // ========== 输入区域 ==========
699
715
  .input-area {
700
716
  padding: 16px 24px 24px;
701
717
  backdrop-filter: blur(10px);
718
+
702
719
  .message-input {
703
720
  :deep(.el-textarea__inner) {
704
721
  border-radius: 16px;
@@ -706,39 +723,33 @@ defineExpose({
706
723
  font-size: 14px;
707
724
  padding: 12px 16px;
708
725
  transition: all 0.3s ease;
709
- &:focus {
710
- border-color: #667eea;
711
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
712
- }
726
+ &:focus { border-color: #667eea; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); }
713
727
  }
714
728
  }
729
+
715
730
  .input-actions {
716
731
  display: flex;
717
732
  justify-content: space-between;
718
733
  align-items: center;
719
734
  margin-top: 12px;
720
- .input-hint {
721
- font-size: 11px;
722
- color: var(--el-color-info);
723
- }
735
+
736
+ .input-hint { font-size: 11px; color: var(--el-color-info); }
737
+
724
738
  .send-btn {
725
739
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
726
740
  border: none;
727
741
  padding: 8px 20px;
728
742
  font-weight: 500;
729
743
  transition: all 0.3s ease;
730
- &:hover {
731
- transform: translateY(-2px);
732
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
733
- }
734
- &:active {
735
- transform: translateY(0);
736
- }
744
+ &:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); }
745
+ &:active { transform: translateY(0); }
737
746
  }
738
747
  }
739
748
  }
740
749
  }
741
750
  }
751
+
752
+ // ========== 思考动画(三个跳动点) ==========
742
753
  .typing-indicator {
743
754
  display: flex;
744
755
  gap: 4px;
@@ -754,75 +765,45 @@ defineExpose({
754
765
  border-radius: 50%;
755
766
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
756
767
  animation: typing 1.4s infinite ease-in-out;
757
-
758
- &:nth-child(1) {
759
- animation-delay: -0.32s;
760
- }
761
- &:nth-child(2) {
762
- animation-delay: -0.16s;
763
- }
768
+ &:nth-child(1) { animation-delay: -0.32s; }
769
+ &:nth-child(2) { animation-delay: -0.16s; }
764
770
  }
765
771
  }
772
+
773
+ // ========== 反馈弹窗 ==========
766
774
  .feedback-dialog-content {
767
775
  text-align: center;
768
776
  padding: 12px 0;
769
777
 
770
- .feedback-emoji {
771
- font-size: 48px;
772
- margin-bottom: 16px;
773
- animation: shake 0.5s ease-in-out;
774
- }
775
-
776
- .feedback-tip {
777
- font-size: 14px;
778
- color: #666;
779
- margin-bottom: 20px;
780
- line-height: 1.6;
781
- }
778
+ .feedback-emoji { font-size: 48px; margin-bottom: 16px; animation: shake 0.5s ease-in-out; }
779
+ .feedback-tip { font-size: 14px; color: #666; margin-bottom: 20px; line-height: 1.6; }
782
780
 
783
781
  :deep(.el-textarea__inner) {
784
782
  border-radius: 12px;
785
783
  border: 1px solid rgba(102, 126, 234, 0.2);
786
784
  font-size: 14px;
787
-
788
- &:focus {
789
- border-color: #667eea;
790
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
791
- }
785
+ &:focus { border-color: #667eea; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); }
792
786
  }
793
787
  }
788
+
789
+ // ========== 动画关键帧 ==========
794
790
  @keyframes fadeInUp {
795
- from {
796
- opacity: 0;
797
- transform: translateY(10px);
798
- }
799
- to {
800
- opacity: 1;
801
- transform: translateY(0);
802
- }
791
+ from { opacity: 0; transform: translateY(10px); }
792
+ to { opacity: 1; transform: translateY(0); }
803
793
  }
794
+
804
795
  @keyframes typing {
805
- 0%,
806
- 60%,
807
- 100% {
808
- transform: translateY(0);
809
- opacity: 0.4;
810
- }
811
- 30% {
812
- transform: translateY(-8px);
813
- opacity: 1;
814
- }
796
+ 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
797
+ 30% { transform: translateY(-8px); opacity: 1; }
815
798
  }
799
+
816
800
  @keyframes shake {
817
- 0%,
818
- 100% {
819
- transform: translateX(0);
820
- }
821
- 25% {
822
- transform: translateX(-5px);
823
- }
824
- 75% {
825
- transform: translateX(5px);
826
- }
801
+ 0%, 100% { transform: translateX(0); }
802
+ 25% { transform: translateX(-5px); }
803
+ 75% { transform: translateX(5px); }
804
+ }
805
+
806
+ @keyframes spin {
807
+ to { transform: rotate(360deg); }
827
808
  }
828
- </style>
809
+ </style>