@liujitcn/kratos-admin-system 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +137 -0
  2. package/dist/package/declarations/index.d.ts +1 -0
  3. package/dist/package/declarations/src/ai.d.ts +10 -0
  4. package/dist/package/declarations/src/api/base/ai_message.d.ts +27 -0
  5. package/dist/package/declarations/src/api/base/ai_session.d.ts +17 -0
  6. package/dist/package/declarations/src/api/base/ai_tool.d.ts +7 -0
  7. package/dist/package/declarations/src/api/base/oauth.d.ts +13 -0
  8. package/dist/package/declarations/src/api/base/sse.d.ts +31 -0
  9. package/dist/package/declarations/src/api/system/auth.d.ts +17 -0
  10. package/dist/package/declarations/src/api/system/base_api.d.ts +22 -0
  11. package/dist/package/declarations/src/api/system/base_area.d.ts +19 -0
  12. package/dist/package/declarations/src/api/system/base_config.d.ts +20 -0
  13. package/dist/package/declarations/src/api/system/base_dept.d.ts +21 -0
  14. package/dist/package/declarations/src/api/system/base_dict.d.ts +32 -0
  15. package/dist/package/declarations/src/api/system/base_job.d.ts +28 -0
  16. package/dist/package/declarations/src/api/system/base_log.d.ts +9 -0
  17. package/dist/package/declarations/src/api/system/base_menu.d.ts +21 -0
  18. package/dist/package/declarations/src/api/system/base_migration.d.ts +10 -0
  19. package/dist/package/declarations/src/api/system/base_post.d.ts +21 -0
  20. package/dist/package/declarations/src/api/system/base_role.d.ts +23 -0
  21. package/dist/package/declarations/src/api/system/base_tenant.d.ts +21 -0
  22. package/dist/package/declarations/src/api/system/base_user.d.ts +23 -0
  23. package/dist/package/declarations/src/api/system/code_gen.d.ts +14 -0
  24. package/dist/package/declarations/src/api/system/code_gen_column.d.ts +12 -0
  25. package/dist/package/declarations/src/api/system/code_gen_proto.d.ts +10 -0
  26. package/dist/package/declarations/src/api/system/code_gen_sse.d.ts +8 -0
  27. package/dist/package/declarations/src/api/system/code_gen_table.d.ts +18 -0
  28. package/dist/package/declarations/src/api/system/project_document.d.ts +9 -0
  29. package/dist/package/declarations/src/components/Ai.vue.d.ts +3 -0
  30. package/dist/package/declarations/src/index.d.ts +3 -0
  31. package/dist/package/declarations/src/module.d.ts +2 -0
  32. package/dist/package/declarations/src/rpc/base/v1/ai_message.d.ts +82 -0
  33. package/dist/package/declarations/src/rpc/base/v1/ai_session.d.ts +176 -0
  34. package/dist/package/declarations/src/rpc/base/v1/ai_tool.d.ts +59 -0
  35. package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +167 -0
  36. package/dist/package/declarations/src/rpc/base/v1/sse.d.ts +13 -0
  37. package/dist/package/declarations/src/rpc/common/v1/common.d.ts +57 -0
  38. package/dist/package/declarations/src/rpc/common/v1/enum.d.ts +58 -0
  39. package/dist/package/declarations/src/rpc/common/v1/types.d.ts +60 -0
  40. package/dist/package/declarations/src/rpc/google/protobuf/empty.d.ts +11 -0
  41. package/dist/package/declarations/src/rpc/google/protobuf/timestamp.d.ts +107 -0
  42. package/dist/package/declarations/src/rpc/system/admin/v1/auth.d.ts +156 -0
  43. package/dist/package/declarations/src/rpc/system/admin/v1/base_api.d.ts +193 -0
  44. package/dist/package/declarations/src/rpc/system/admin/v1/base_area.d.ts +82 -0
  45. package/dist/package/declarations/src/rpc/system/admin/v1/base_config.d.ts +112 -0
  46. package/dist/package/declarations/src/rpc/system/admin/v1/base_dept.d.ts +112 -0
  47. package/dist/package/declarations/src/rpc/system/admin/v1/base_dict.d.ts +215 -0
  48. package/dist/package/declarations/src/rpc/system/admin/v1/base_job.d.ts +177 -0
  49. package/dist/package/declarations/src/rpc/system/admin/v1/base_log.d.ts +87 -0
  50. package/dist/package/declarations/src/rpc/system/admin/v1/base_menu.d.ts +151 -0
  51. package/dist/package/declarations/src/rpc/system/admin/v1/base_migration.d.ts +64 -0
  52. package/dist/package/declarations/src/rpc/system/admin/v1/base_post.d.ts +112 -0
  53. package/dist/package/declarations/src/rpc/system/admin/v1/base_role.d.ts +128 -0
  54. package/dist/package/declarations/src/rpc/system/admin/v1/base_tenant.d.ts +112 -0
  55. package/dist/package/declarations/src/rpc/system/admin/v1/base_user.d.ts +161 -0
  56. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen.d.ts +155 -0
  57. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen_column.d.ts +143 -0
  58. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen_proto.d.ts +86 -0
  59. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen_table.d.ts +149 -0
  60. package/dist/package/declarations/src/rpc/system/admin/v1/project_document.d.ts +64 -0
  61. package/dist/package/declarations/src/rpc/system/common/v1/enum.d.ts +67 -0
  62. package/dist/package/declarations/src/views/ai/chat/attachment.d.ts +14 -0
  63. package/dist/package/declarations/src/views/ai/chat/components/AiMarkdown.vue.d.ts +13 -0
  64. package/dist/package/declarations/src/views/ai/chat/components/ChatPanel.vue.d.ts +45 -0
  65. package/dist/package/declarations/src/views/ai/chat/components/SessionPanel.vue.d.ts +34 -0
  66. package/dist/package/declarations/src/views/ai/chat/components/XSender.vue.d.ts +12 -0
  67. package/dist/package/declarations/src/views/ai/chat/index.vue.d.ts +4 -0
  68. package/dist/package/declarations/src/views/ai/chat/message.d.ts +59 -0
  69. package/dist/package/declarations/src/views/ai/chat/stream.d.ts +10 -0
  70. package/dist/package/declarations/src/views/ai/chat/types.d.ts +115 -0
  71. package/dist/package/declarations/src/views/base/api/index.vue.d.ts +3 -0
  72. package/dist/package/declarations/src/views/base/area/index.vue.d.ts +3 -0
  73. package/dist/package/declarations/src/views/base/config/index.vue.d.ts +3 -0
  74. package/dist/package/declarations/src/views/base/dept/index.vue.d.ts +3 -0
  75. package/dist/package/declarations/src/views/base/dict/index.vue.d.ts +3 -0
  76. package/dist/package/declarations/src/views/base/dict/item.vue.d.ts +3 -0
  77. package/dist/package/declarations/src/views/base/job/index.vue.d.ts +3 -0
  78. package/dist/package/declarations/src/views/base/job/log.vue.d.ts +3 -0
  79. package/dist/package/declarations/src/views/base/log/index.vue.d.ts +3 -0
  80. package/dist/package/declarations/src/views/base/menu/index.vue.d.ts +3 -0
  81. package/dist/package/declarations/src/views/base/migration/index.vue.d.ts +4 -0
  82. package/dist/package/declarations/src/views/base/post/index.vue.d.ts +3 -0
  83. package/dist/package/declarations/src/views/base/role/index.vue.d.ts +3 -0
  84. package/dist/package/declarations/src/views/base/tenant/index.vue.d.ts +3 -0
  85. package/dist/package/declarations/src/views/base/user/components/dept-tree.vue.d.ts +19 -0
  86. package/dist/package/declarations/src/views/base/user/index.vue.d.ts +3 -0
  87. package/dist/package/declarations/src/views/profile/components/base.vue.d.ts +13 -0
  88. package/dist/package/declarations/src/views/profile/components/password.vue.d.ts +3 -0
  89. package/dist/package/declarations/src/views/profile/components/security.vue.d.ts +15 -0
  90. package/dist/package/declarations/src/views/profile/index.vue.d.ts +3 -0
  91. package/dist/package/declarations/src/views/tool/api-doc/index.vue.d.ts +4 -0
  92. package/dist/package/declarations/src/views/tool/code-gen/code-preview/index.vue.d.ts +3 -0
  93. package/dist/package/declarations/src/views/tool/code-gen/columns/index.vue.d.ts +3 -0
  94. package/dist/package/declarations/src/views/tool/code-gen/columns/option-copy.d.ts +25 -0
  95. package/dist/package/declarations/src/views/tool/code-gen/components/CodeGenProgressDialog.vue.d.ts +16 -0
  96. package/dist/package/declarations/src/views/tool/code-gen/components/CodePreviewPane.vue.d.ts +9 -0
  97. package/dist/package/declarations/src/views/tool/code-gen/config.d.ts +32 -0
  98. package/dist/package/declarations/src/views/tool/code-gen/preview/capabilities.d.ts +20 -0
  99. package/dist/package/declarations/src/views/tool/code-gen/preview/data.d.ts +42 -0
  100. package/dist/package/declarations/src/views/tool/code-gen/preview/index.vue.d.ts +3 -0
  101. package/dist/package/declarations/src/views/tool/code-gen/proto/index.vue.d.ts +3 -0
  102. package/dist/package/declarations/src/views/tool/code-gen/table/index.vue.d.ts +3 -0
  103. package/dist/package/declarations/src/views/tool/project-doc/index.vue.d.ts +4 -0
  104. package/dist/package/src/ai.ts +16 -0
  105. package/dist/package/src/api/base/ai_message.ts +139 -0
  106. package/dist/package/src/api/base/ai_session.ts +77 -0
  107. package/dist/package/src/api/base/ai_tool.ts +22 -0
  108. package/dist/package/src/api/base/oauth.ts +47 -0
  109. package/dist/package/src/api/base/sse.ts +192 -0
  110. package/dist/package/src/api/system/auth.ts +65 -0
  111. package/dist/package/src/api/system/base_api.ts +95 -0
  112. package/dist/package/src/api/system/base_area.ts +73 -0
  113. package/dist/package/src/api/system/base_config.ts +82 -0
  114. package/dist/package/src/api/system/base_dept.ts +83 -0
  115. package/dist/package/src/api/system/base_dict.ts +144 -0
  116. package/dist/package/src/api/system/base_job.ts +125 -0
  117. package/dist/package/src/api/system/base_log.ts +32 -0
  118. package/dist/package/src/api/system/base_menu.ts +83 -0
  119. package/dist/package/src/api/system/base_migration.ts +35 -0
  120. package/dist/package/src/api/system/base_post.ts +83 -0
  121. package/dist/package/src/api/system/base_role.ts +93 -0
  122. package/dist/package/src/api/system/base_tenant.ts +83 -0
  123. package/dist/package/src/api/system/base_user.ts +93 -0
  124. package/dist/package/src/api/system/code_gen.ts +56 -0
  125. package/dist/package/src/api/system/code_gen_column.ts +43 -0
  126. package/dist/package/src/api/system/code_gen_proto.ts +32 -0
  127. package/dist/package/src/api/system/code_gen_sse.ts +33 -0
  128. package/dist/package/src/api/system/code_gen_table.ts +74 -0
  129. package/dist/package/src/api/system/project_document.ts +33 -0
  130. package/dist/package/src/components/Ai.vue +57 -0
  131. package/dist/package/src/index.ts +3 -0
  132. package/dist/package/src/module.ts +17 -0
  133. package/dist/package/src/rpc/base/v1/ai_message.ts +98 -0
  134. package/dist/package/src/rpc/base/v1/ai_session.ts +214 -0
  135. package/dist/package/src/rpc/base/v1/ai_tool.ts +74 -0
  136. package/dist/package/src/rpc/base/v1/oauth.ts +199 -0
  137. package/dist/package/src/rpc/base/v1/sse.ts +22 -0
  138. package/dist/package/src/rpc/common/v1/common.ts +71 -0
  139. package/dist/package/src/rpc/common/v1/enum.ts +71 -0
  140. package/dist/package/src/rpc/common/v1/types.ts +77 -0
  141. package/dist/package/src/rpc/google/protobuf/empty.ts +19 -0
  142. package/dist/package/src/rpc/google/protobuf/timestamp.ts +115 -0
  143. package/dist/package/src/rpc/system/admin/v1/auth.ts +208 -0
  144. package/dist/package/src/rpc/system/admin/v1/base_api.ts +241 -0
  145. package/dist/package/src/rpc/system/admin/v1/base_area.ts +105 -0
  146. package/dist/package/src/rpc/system/admin/v1/base_config.ts +140 -0
  147. package/dist/package/src/rpc/system/admin/v1/base_dept.ts +140 -0
  148. package/dist/package/src/rpc/system/admin/v1/base_dict.ts +249 -0
  149. package/dist/package/src/rpc/system/admin/v1/base_job.ts +206 -0
  150. package/dist/package/src/rpc/system/admin/v1/base_log.ts +101 -0
  151. package/dist/package/src/rpc/system/admin/v1/base_menu.ts +195 -0
  152. package/dist/package/src/rpc/system/admin/v1/base_migration.ts +81 -0
  153. package/dist/package/src/rpc/system/admin/v1/base_post.ts +134 -0
  154. package/dist/package/src/rpc/system/admin/v1/base_role.ts +151 -0
  155. package/dist/package/src/rpc/system/admin/v1/base_tenant.ts +134 -0
  156. package/dist/package/src/rpc/system/admin/v1/base_user.ts +190 -0
  157. package/dist/package/src/rpc/system/admin/v1/code_gen.ts +176 -0
  158. package/dist/package/src/rpc/system/admin/v1/code_gen_column.ts +170 -0
  159. package/dist/package/src/rpc/system/admin/v1/code_gen_proto.ts +104 -0
  160. package/dist/package/src/rpc/system/admin/v1/code_gen_table.ts +179 -0
  161. package/dist/package/src/rpc/system/admin/v1/project_document.ts +79 -0
  162. package/dist/package/src/rpc/system/common/v1/enum.ts +80 -0
  163. package/dist/package/src/typings/swagger-ui-dist.d.ts +28 -0
  164. package/dist/package/src/typings/x-markdown-vue.d.ts +1 -0
  165. package/dist/package/src/views/ai/chat/attachment.ts +65 -0
  166. package/dist/package/src/views/ai/chat/components/AiMarkdown.vue +162 -0
  167. package/dist/package/src/views/ai/chat/components/ChatPanel.vue +1861 -0
  168. package/dist/package/src/views/ai/chat/components/SessionPanel.vue +316 -0
  169. package/dist/package/src/views/ai/chat/components/XSender.vue +554 -0
  170. package/dist/package/src/views/ai/chat/index.vue +848 -0
  171. package/dist/package/src/views/ai/chat/message.ts +416 -0
  172. package/dist/package/src/views/ai/chat/stream.ts +115 -0
  173. package/dist/package/src/views/ai/chat/types.ts +126 -0
  174. package/dist/package/src/views/base/api/index.vue +455 -0
  175. package/dist/package/src/views/base/area/index.vue +269 -0
  176. package/dist/package/src/views/base/config/index.vue +530 -0
  177. package/dist/package/src/views/base/dept/index.vue +461 -0
  178. package/dist/package/src/views/base/dict/index.vue +304 -0
  179. package/dist/package/src/views/base/dict/item.vue +373 -0
  180. package/dist/package/src/views/base/job/index.vue +535 -0
  181. package/dist/package/src/views/base/job/log.vue +202 -0
  182. package/dist/package/src/views/base/log/index.vue +317 -0
  183. package/dist/package/src/views/base/menu/index.vue +944 -0
  184. package/dist/package/src/views/base/migration/index.vue +791 -0
  185. package/dist/package/src/views/base/post/index.vue +324 -0
  186. package/dist/package/src/views/base/role/index.vue +684 -0
  187. package/dist/package/src/views/base/tenant/index.vue +328 -0
  188. package/dist/package/src/views/base/user/components/dept-tree.vue +75 -0
  189. package/dist/package/src/views/base/user/index.vue +783 -0
  190. package/dist/package/src/views/profile/components/base.vue +320 -0
  191. package/dist/package/src/views/profile/components/password.vue +269 -0
  192. package/dist/package/src/views/profile/components/security.vue +443 -0
  193. package/dist/package/src/views/profile/index.vue +199 -0
  194. package/dist/package/src/views/tool/api-doc/index.vue +275 -0
  195. package/dist/package/src/views/tool/code-gen/code-preview/index.vue +233 -0
  196. package/dist/package/src/views/tool/code-gen/columns/index.vue +1456 -0
  197. package/dist/package/src/views/tool/code-gen/columns/option-copy.ts +125 -0
  198. package/dist/package/src/views/tool/code-gen/components/CodeGenProgressDialog.vue +328 -0
  199. package/dist/package/src/views/tool/code-gen/components/CodePreviewPane.vue +42 -0
  200. package/dist/package/src/views/tool/code-gen/config.ts +175 -0
  201. package/dist/package/src/views/tool/code-gen/preview/capabilities.ts +34 -0
  202. package/dist/package/src/views/tool/code-gen/preview/data.ts +331 -0
  203. package/dist/package/src/views/tool/code-gen/preview/index.vue +831 -0
  204. package/dist/package/src/views/tool/code-gen/proto/index.vue +703 -0
  205. package/dist/package/src/views/tool/code-gen/table/index.vue +864 -0
  206. package/dist/package/src/views/tool/project-doc/index.vue +561 -0
  207. package/package.json +60 -0
  208. package/tsconfig.json +13 -0
@@ -0,0 +1,554 @@
1
+ <template>
2
+ <div class="agent-sender-wrap">
3
+ <BaseXSender
4
+ ref="senderRef"
5
+ class="agent-sender"
6
+ variant="updown"
7
+ submit-type="enter"
8
+ placeholder="输入任何问题,或上传附件后继续提问"
9
+ :loading="sending"
10
+ :clearable="true"
11
+ :tip-config="false"
12
+ @change="handleInputChange"
13
+ @submit="handleSubmit"
14
+ @paste-file="handlePasteFile"
15
+ >
16
+ <template v-if="selectedAttachments.length" #header>
17
+ <Attachments
18
+ class="agent-attachments"
19
+ :items="attachmentItems"
20
+ overflow="wrap"
21
+ :hide-upload="true"
22
+ @delete-card="handleDeleteCard"
23
+ />
24
+ </template>
25
+
26
+ <template #prefix>
27
+ <div class="agent-prefix-actions">
28
+ <el-popover placement="top-start" :width="236" trigger="click" popper-class="agent-sender-popover">
29
+ <template #reference>
30
+ <button class="agent-icon-button" type="button" :disabled="sending || uploading" aria-label="上传附件">
31
+ <el-icon :class="{ 'is-loading': uploading }">
32
+ <Loading v-if="uploading" />
33
+ <Paperclip v-else />
34
+ </el-icon>
35
+ </button>
36
+ </template>
37
+ <div class="agent-popover-card">
38
+ <div class="agent-popover-title">上传附件</div>
39
+ <div class="agent-popover-desc">可补充图片或文本类附件,发送后用于当前问题分析。</div>
40
+ <button class="agent-popover-action" type="button" :disabled="sending || uploading" @click="handleSelectAttachment">
41
+ {{ uploading ? "上传中..." : "选择本地文件" }}
42
+ </button>
43
+ </div>
44
+ </el-popover>
45
+
46
+ <span class="agent-action-text">{{ actionHintText }}</span>
47
+ </div>
48
+ </template>
49
+
50
+ <template #action-list>
51
+ <div class="agent-sender-actions">
52
+ <el-tooltip :content="recording ? '停止语音输入' : '语音输入'" placement="top">
53
+ <button
54
+ class="agent-icon-button"
55
+ :class="{ 'is-active': recording }"
56
+ type="button"
57
+ :disabled="sending"
58
+ :aria-pressed="recording"
59
+ :aria-label="recording ? '停止语音输入' : '语音输入'"
60
+ @click="handleToggleRecord"
61
+ >
62
+ <el-icon :class="{ 'is-loading': recording }">
63
+ <Loading v-if="recording" />
64
+ <Microphone v-else />
65
+ </el-icon>
66
+ </button>
67
+ </el-tooltip>
68
+ <el-tooltip content="发送" placement="top">
69
+ <button
70
+ class="agent-send-button"
71
+ type="button"
72
+ :disabled="sending || uploading || recording || isSubmitDisabled"
73
+ aria-label="发送"
74
+ @click="handleSubmit()"
75
+ >
76
+ <el-icon v-if="!sending"><Promotion /></el-icon>
77
+ <el-icon v-else class="is-loading"><Loading /></el-icon>
78
+ </button>
79
+ </el-tooltip>
80
+ </div>
81
+ </template>
82
+ </BaseXSender>
83
+
84
+ <input
85
+ ref="fileInputRef"
86
+ class="agent-file-input"
87
+ type="file"
88
+ multiple
89
+ :accept="acceptedAttachmentTypes"
90
+ @change="handleFileChange"
91
+ />
92
+ </div>
93
+ </template>
94
+
95
+ <script setup lang="ts" name="XSender">
96
+ import { computed, ref, watch } from "vue";
97
+ import { Attachments, useRecord, XSender as BaseXSender } from "vue-element-plus-x";
98
+ import type { FilesCardProps } from "vue-element-plus-x/types/FilesCard";
99
+ import { Loading, Microphone, Paperclip, Promotion } from "@element-plus/icons-vue";
100
+ import { ElMessage } from "element-plus";
101
+ import { defFileService } from "@liujitcn/kratos-admin-core/api/base/file";
102
+ import type { AiAttachment } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
103
+ import type { SubmitPayload } from "../types";
104
+ import { buildAIAttachmentFileCard } from "../attachment";
105
+
106
+ /** 语音识别错误的最小字段,兼容浏览器事件和不支持错误。 */
107
+ type RecordError = {
108
+ code?: number;
109
+ error?: string;
110
+ message?: string;
111
+ };
112
+
113
+ const props = defineProps<{
114
+ /** 消息发送加载状态。 */
115
+ sending: boolean;
116
+ }>();
117
+
118
+ const emit = defineEmits<{
119
+ /** 提交输入器内容。 */
120
+ submit: [payload: SubmitPayload];
121
+ }>();
122
+
123
+ const senderRef = ref<InstanceType<typeof BaseXSender>>();
124
+ const fileInputRef = ref<HTMLInputElement>();
125
+ const inputText = ref("");
126
+ const selectedAttachments = ref<AiAttachment[]>([]);
127
+ const uploading = ref(false);
128
+ const maxAttachmentCount = 6;
129
+ const maxAttachmentSizeMB = 20;
130
+ const maxAttachmentSize = maxAttachmentSizeMB * 1024 * 1024;
131
+ const acceptedAttachmentExtensions = [
132
+ ".txt",
133
+ ".md",
134
+ ".markdown",
135
+ ".log",
136
+ ".json",
137
+ ".xml",
138
+ ".csv",
139
+ ".png",
140
+ ".jpg",
141
+ ".jpeg",
142
+ ".gif",
143
+ ".webp"
144
+ ];
145
+ const acceptedAttachmentTypes = acceptedAttachmentExtensions.join(",");
146
+
147
+ const {
148
+ loading: recording,
149
+ start: startRecord,
150
+ stop: stopRecord,
151
+ value: recordText
152
+ } = useRecord({
153
+ onEnd: handleRecordEnd,
154
+ onError: handleRecordError
155
+ });
156
+
157
+ const actionHintText = computed(() => {
158
+ if (recording.value) return "正在识别语音...";
159
+ if (uploading.value) return "附件上传中...";
160
+ if (selectedAttachments.value.length) return `已选 ${selectedAttachments.value.length} 个附件`;
161
+ return "可上传附件";
162
+ });
163
+
164
+ const attachmentItems = computed<FilesCardProps[]>(() =>
165
+ selectedAttachments.value.map(item =>
166
+ buildAIAttachmentFileCard(item, {
167
+ showDelIcon: true,
168
+ maxWidth: "220px"
169
+ })
170
+ )
171
+ );
172
+
173
+ const isSubmitDisabled = computed(() => {
174
+ return uploading.value || (!inputText.value.trim() && selectedAttachments.value.length === 0);
175
+ });
176
+
177
+ /** 读取输入内容并发送给父组件。 */
178
+ function handleSubmit() {
179
+ if (uploading.value) return;
180
+ if (recording.value) {
181
+ stopRecord();
182
+ return;
183
+ }
184
+ const trimmedText = inputText.value.trim();
185
+ if (!trimmedText && selectedAttachments.value.length === 0) return;
186
+
187
+ emit("submit", {
188
+ text: trimmedText || "请结合附件内容继续分析",
189
+ attachments: [...selectedAttachments.value]
190
+ });
191
+ inputText.value = "";
192
+ selectedAttachments.value = [];
193
+ senderRef.value?.clear();
194
+ resetFileInput();
195
+ }
196
+
197
+ /** 切换浏览器语音识别状态。 */
198
+ function handleToggleRecord() {
199
+ if (recording.value) {
200
+ stopRecord();
201
+ return;
202
+ }
203
+ startRecord();
204
+ }
205
+
206
+ /** 语音识别结束后保留当前识别文本。 */
207
+ function handleRecordEnd(result: string) {
208
+ setRecordText(result);
209
+ }
210
+
211
+ /** 处理语音识别失败,提示浏览器不支持或麦克风授权异常。 */
212
+ function handleRecordError(error: RecordError) {
213
+ const message = resolveRecordErrorMessage(error);
214
+ ElMessage.warning(message);
215
+ }
216
+
217
+ /** 将识别文本与识别前内容合并后写回输入器。 */
218
+ function setRecordText(recordText: string) {
219
+ const normalizedRecordText = normalizeRecordText(recordText);
220
+ if (!normalizedRecordText) return;
221
+ senderRef.value?.setText(normalizedRecordText);
222
+ inputText.value = normalizedRecordText;
223
+ }
224
+
225
+ /** 标准化语音识别文本,压掉多余空白。 */
226
+ function normalizeRecordText(recordText: string) {
227
+ return collapseCumulativeRecordText(recordText.replace(/\s+/g, " ").trim());
228
+ }
229
+
230
+ /** 压缩 useRecord 在中文连续识别时返回的前缀累计文本。 */
231
+ function collapseCumulativeRecordText(text: string) {
232
+ if (text.length < 4) return text;
233
+ for (let candidateLength = 2; candidateLength <= Math.floor(text.length / 2); candidateLength++) {
234
+ const candidate = text.slice(-candidateLength);
235
+ const matchResult = matchCumulativeRecordCandidate(text, candidate);
236
+ if (matchResult.matched && (matchResult.hasPartial || matchResult.segmentCount >= 3)) {
237
+ return candidate;
238
+ }
239
+ }
240
+ return text;
241
+ }
242
+
243
+ /** 判断文本是否由同一识别结果的前缀片段累计组成。 */
244
+ function matchCumulativeRecordCandidate(text: string, candidate: string) {
245
+ let index = 0;
246
+ let segmentCount = 0;
247
+ let hasPartial = false;
248
+ while (index < text.length) {
249
+ let matchedLength = 0;
250
+ const maxLength = Math.min(candidate.length, text.length - index);
251
+ for (let length = maxLength; length >= 1; length--) {
252
+ if (candidate.startsWith(text.slice(index, index + length))) {
253
+ matchedLength = length;
254
+ break;
255
+ }
256
+ }
257
+ if (!matchedLength) return { matched: false, segmentCount, hasPartial };
258
+ if (matchedLength < candidate.length) hasPartial = true;
259
+ segmentCount++;
260
+ index += matchedLength;
261
+ }
262
+ return { matched: true, segmentCount, hasPartial };
263
+ }
264
+
265
+ /** 根据浏览器语音识别错误类型生成用户可理解的提示。 */
266
+ function resolveRecordErrorMessage(error: RecordError) {
267
+ if (error.code === -1) return "当前浏览器不支持语音识别";
268
+ const errorName = error.error ?? "";
269
+ if (["not-allowed", "service-not-allowed", "permission-denied"].includes(errorName)) return "麦克风授权失败,请检查浏览器权限";
270
+ return "语音识别失败,请稍后重试";
271
+ }
272
+
273
+ /** 同步输入器内部文本,保证发送按钮禁用态能实时响应。 */
274
+ function handleInputChange() {
275
+ inputText.value = senderRef.value?.getModelValue().text ?? "";
276
+ }
277
+
278
+ /** 打开本地文件选择框。 */
279
+ function handleSelectAttachment() {
280
+ if (uploading.value || props.sending) return;
281
+ fileInputRef.value?.click();
282
+ }
283
+
284
+ /** 将本地文件列表上传后同步到输入器附件区。 */
285
+ async function handleFileChange(event: Event) {
286
+ const target = event.target as HTMLInputElement;
287
+ await uploadAttachments(Array.from(target.files ?? []));
288
+ }
289
+
290
+ /** 处理粘贴文件,和点击上传保持同一份附件状态。 */
291
+ async function handlePasteFile(firstFile: File, fileList: FileList) {
292
+ await uploadAttachments([firstFile, ...Array.from(fileList).slice(1)]);
293
+ }
294
+
295
+ /** 上传附件并按文件地址去重。 */
296
+ async function uploadAttachments(files: File[]) {
297
+ if (uploading.value || props.sending) return;
298
+ const uploadFiles = filterUploadFiles(files);
299
+ if (!uploadFiles.length) return;
300
+ uploading.value = true;
301
+ try {
302
+ const response = await defFileService.MultiUploadFile(uploadFiles, "ai");
303
+ const fileMap = new Map<string, File[]>();
304
+ uploadFiles.forEach(file => {
305
+ const group = fileMap.get(file.name) ?? [];
306
+ group.push(file);
307
+ fileMap.set(file.name, group);
308
+ });
309
+ const attachmentMap = new Map(selectedAttachments.value.map(item => [item.url || item.id, item]));
310
+ response?.files?.forEach(item => {
311
+ const matchedFile = fileMap.get(item.name || "")?.shift();
312
+ const attachment = buildAttachmentItem(item.url || "", item.name || "", matchedFile);
313
+ attachmentMap.set(attachment.url || attachment.id, attachment);
314
+ });
315
+ selectedAttachments.value = Array.from(attachmentMap.values());
316
+ if (response?.files?.length) {
317
+ ElMessage.success(`已上传 ${response.files.length} 个附件`);
318
+ }
319
+ } catch {
320
+ ElMessage.error("附件上传失败");
321
+ } finally {
322
+ uploading.value = false;
323
+ resetFileInput();
324
+ }
325
+ }
326
+
327
+ /** 过滤不符合数量和大小约束的附件。 */
328
+ function filterUploadFiles(files: File[]) {
329
+ const remainingCount = maxAttachmentCount - selectedAttachments.value.length;
330
+ if (remainingCount <= 0) {
331
+ ElMessage.warning(`最多上传 ${maxAttachmentCount} 个附件`);
332
+ return [];
333
+ }
334
+ const validFiles = files.filter(file => {
335
+ if (!isAcceptedAttachmentFile(file)) {
336
+ ElMessage.warning(`附件「${file.name}」暂不支持,已跳过`);
337
+ return false;
338
+ }
339
+ if (file.size > maxAttachmentSize) {
340
+ ElMessage.warning(`附件「${file.name}」超过 ${maxAttachmentSizeMB}MB,已跳过`);
341
+ return false;
342
+ }
343
+ return true;
344
+ });
345
+ if (validFiles.length > remainingCount) {
346
+ ElMessage.warning(`最多还能选择 ${remainingCount} 个附件,已自动截取`);
347
+ }
348
+ return validFiles.slice(0, remainingCount);
349
+ }
350
+
351
+ /** 判断附件类型是否在当前 AI 助手可解析范围内。 */
352
+ function isAcceptedAttachmentFile(file: File) {
353
+ const fileName = file.name.toLowerCase();
354
+ return acceptedAttachmentExtensions.some(extension => fileName.endsWith(extension));
355
+ }
356
+
357
+ /** 根据上传结果构建附件展示项。 */
358
+ function buildAttachmentItem(url: string, name: string, file?: File): AiAttachment {
359
+ return {
360
+ id: url || `${name}-${file?.size ?? 0}-${file?.lastModified ?? Date.now()}`,
361
+ name,
362
+ size: file?.size ?? 0,
363
+ url,
364
+ mime_type: file?.type ?? ""
365
+ };
366
+ }
367
+
368
+ /** 复用 Attachments 删除事件,保持附件状态与组件展示同步。 */
369
+ function handleDeleteCard(item: { uid?: string | number }) {
370
+ if (!item.uid) return;
371
+ selectedAttachments.value = selectedAttachments.value.filter(attachment => attachment.id !== String(item.uid));
372
+ if (!selectedAttachments.value.length) resetFileInput();
373
+ }
374
+
375
+ /** 清空文件输入框,保证重复选择同名文件时仍能触发 change。 */
376
+ function resetFileInput() {
377
+ if (fileInputRef.value) fileInputRef.value.value = "";
378
+ }
379
+
380
+ /** 按 useRecord 文档监听识别文本,并同步到 XSender。 */
381
+ watch(
382
+ recordText,
383
+ value => {
384
+ setRecordText(value);
385
+ },
386
+ { deep: true }
387
+ );
388
+ </script>
389
+
390
+ <style scoped lang="scss">
391
+ .agent-sender-wrap {
392
+ width: 100%;
393
+ }
394
+ .agent-sender {
395
+ :deep(.elx-x-sender) {
396
+ border-radius: var(--admin-page-radius);
397
+ box-shadow: none;
398
+ }
399
+ :deep(.elx-x-sender__content) {
400
+ padding: 8px 10px 10px;
401
+ background: var(--admin-page-card-bg);
402
+ border: 1px solid var(--admin-page-card-border);
403
+ border-radius: var(--admin-page-radius);
404
+ }
405
+ :deep(.elx-x-sender__content--variant-updown) {
406
+ gap: 10px;
407
+ }
408
+ :deep(.chat-rich-text) {
409
+ min-height: 64px;
410
+ max-height: 120px;
411
+ padding: 8px 10px;
412
+ font-size: 14px;
413
+ line-height: 22px;
414
+ }
415
+ :deep(.chat-placeholder-wrap) {
416
+ padding: 8px 10px;
417
+ font-size: 14px;
418
+ font-weight: 400;
419
+ }
420
+ :deep(.elx-x-sender__updown-action-list) {
421
+ align-items: center;
422
+ justify-content: space-between;
423
+ }
424
+ :deep(.elx-x-sender__action-list) {
425
+ height: auto;
426
+ }
427
+ }
428
+ .agent-attachments {
429
+ padding: 12px 12px 0;
430
+ :deep(.elx-files-card) {
431
+ max-width: 220px;
432
+ border-radius: var(--admin-page-radius);
433
+ }
434
+ :deep(.elx-files-card-img),
435
+ :deep(.elx-files-card__image-preview),
436
+ :deep(.elx-files-card-delete-icon) {
437
+ border-radius: var(--admin-page-radius);
438
+ }
439
+ }
440
+ .agent-prefix-actions {
441
+ display: flex;
442
+ gap: 10px;
443
+ align-items: center;
444
+ min-height: 36px;
445
+ }
446
+ .agent-sender-actions {
447
+ display: inline-flex;
448
+ gap: 8px;
449
+ align-items: center;
450
+ }
451
+ .agent-icon-button,
452
+ .agent-send-button {
453
+ display: inline-flex;
454
+ align-items: center;
455
+ justify-content: center;
456
+ width: 34px;
457
+ height: 34px;
458
+ color: var(--admin-page-text-secondary);
459
+ cursor: pointer;
460
+ background: var(--admin-page-card-bg);
461
+ border: 1px solid var(--el-border-color);
462
+ border-radius: var(--admin-page-radius);
463
+ transition:
464
+ color 0.2s ease,
465
+ border-color 0.2s ease,
466
+ background-color 0.2s ease,
467
+ opacity 0.2s ease;
468
+ &:hover {
469
+ color: var(--el-color-primary);
470
+ background: var(--el-color-primary-light-9);
471
+ border-color: var(--el-color-primary-light-5);
472
+ }
473
+ &:disabled {
474
+ cursor: not-allowed;
475
+ opacity: 0.55;
476
+ }
477
+ }
478
+ .agent-send-button {
479
+ color: var(--el-color-primary);
480
+ background: var(--el-color-primary-light-9);
481
+ border-color: var(--el-color-primary-light-5);
482
+ }
483
+ .agent-icon-button.is-active {
484
+ color: var(--el-color-primary);
485
+ background: var(--el-color-primary-light-9);
486
+ border-color: var(--el-color-primary-light-5);
487
+ }
488
+ .agent-action-text {
489
+ font-size: 12px;
490
+ line-height: 18px;
491
+ color: var(--admin-page-text-secondary);
492
+ }
493
+ .is-loading {
494
+ animation: rotating 1s linear infinite;
495
+ }
496
+ .agent-file-input {
497
+ display: none;
498
+ }
499
+ :global(.agent-sender-popover) {
500
+ padding: 0 !important;
501
+ border-radius: var(--admin-page-radius) !important;
502
+ }
503
+ .agent-popover-card {
504
+ padding: 14px;
505
+ }
506
+ .agent-popover-title {
507
+ font-size: 14px;
508
+ font-weight: 700;
509
+ color: var(--admin-page-text-primary);
510
+ }
511
+ .agent-popover-desc {
512
+ margin-top: 6px;
513
+ font-size: 12px;
514
+ line-height: 20px;
515
+ color: var(--admin-page-text-secondary);
516
+ }
517
+ .agent-popover-action {
518
+ width: 100%;
519
+ height: 36px;
520
+ margin-top: 12px;
521
+ font-size: 13px;
522
+ font-weight: 600;
523
+ color: var(--el-color-primary);
524
+ cursor: pointer;
525
+ background: var(--el-color-primary-light-9);
526
+ border: 0;
527
+ border-radius: var(--admin-page-radius);
528
+ &:disabled {
529
+ cursor: not-allowed;
530
+ opacity: 0.55;
531
+ }
532
+ }
533
+ :global(.agent-sender-popover.el-popover) {
534
+ box-shadow: 0 14px 36px rgb(15 23 42 / 10%);
535
+ }
536
+
537
+ @media screen and (width <= 768px) {
538
+ .agent-prefix-actions {
539
+ gap: 8px;
540
+ }
541
+ .agent-action-text {
542
+ display: none;
543
+ }
544
+ }
545
+
546
+ @keyframes rotating {
547
+ from {
548
+ transform: rotate(0deg);
549
+ }
550
+ to {
551
+ transform: rotate(360deg);
552
+ }
553
+ }
554
+ </style>